diff --git a/.editorconfig b/.editorconfig index 58ba190d..d102bc5a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -23,6 +23,7 @@ max_line_length = off [*.{csv,json,html,md,po,py,svg,tsv}] max_line_length = off +# ` ` at the end of a line is a line-break in markdown [*.{md,markdown}] trim_trailing_whitespace = false @@ -30,7 +31,9 @@ trim_trailing_whitespace = false indent_size = 2 max_line_length = off -[{package.json,yarn.lock}] +# Computer generated files +[{package.json,*.lock,*.mo}] indent_size = unset indent_style = unset max_line_length = unset +insert_final_newline = unset diff --git a/.env.example b/.env.dev.example similarity index 74% rename from .env.example rename to .env.dev.example index cf3705af..5e605d74 100644 --- a/.env.example +++ b/.env.dev.example @@ -5,6 +5,7 @@ SECRET_KEY="7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr" DEBUG=true DOMAIN=your.domain.here +#EMAIL=your@email.here ## Leave unset to allow all hosts # ALLOWED_HOSTS="localhost,127.0.0.1,[::1]" @@ -26,13 +27,24 @@ POSTGRES_HOST=db MAX_STREAM_LENGTH=200 REDIS_ACTIVITY_HOST=redis_activity REDIS_ACTIVITY_PORT=6379 +#REDIS_ACTIVITY_PASSWORD=redispassword345 -# Celery config with redis broker +# Redis as celery broker +#REDIS_BROKER_PORT=6379 +#REDIS_BROKER_PASSWORD=redispassword123 CELERY_BROKER=redis://redis_broker:6379/0 CELERY_RESULT_BACKEND=redis://redis_broker:6379/0 +FLOWER_PORT=8888 +#FLOWER_USER=mouse +#FLOWER_PASSWORD=changeme + EMAIL_HOST="smtp.mailgun.org" EMAIL_PORT=587 EMAIL_HOST_USER=mail@your.domain.here EMAIL_HOST_PASSWORD=emailpassword123 EMAIL_USE_TLS=true +EMAIL_USE_SSL=false + +# Set this to true when initializing certbot for domain, false when not +CERTBOT_INIT=false diff --git a/.env.prod.example b/.env.prod.example new file mode 100644 index 00000000..0013bf9d --- /dev/null +++ b/.env.prod.example @@ -0,0 +1,50 @@ +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY="7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG=false + +DOMAIN=your.domain.here +EMAIL=your@email.here + +## Leave unset to allow all hosts +# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]" + +OL_URL=https://openlibrary.org + +## Database backend to use. +## Default is postgres, sqlite is for dev quickstart only (NOT production!!!) +BOOKWYRM_DATABASE_BACKEND=postgres + +MEDIA_ROOT=images/ + +POSTGRES_PASSWORD=securedbpassword123 +POSTGRES_USER=fedireads +POSTGRES_DB=fedireads +POSTGRES_HOST=db + +# Redis activity stream manager +MAX_STREAM_LENGTH=200 +REDIS_ACTIVITY_HOST=redis_activity +REDIS_ACTIVITY_PORT=6379 +REDIS_ACTIVITY_PASSWORD=redispassword345 + +# Redis as celery broker +REDIS_BROKER_PORT=6379 +REDIS_BROKER_PASSWORD=redispassword123 +CELERY_BROKER=redis://:${REDIS_BROKER_PASSWORD}@redis_broker:${REDIS_BROKER_PORT}/0 +CELERY_RESULT_BACKEND=redis://:${REDIS_BROKER_PASSWORD}@redis_broker:${REDIS_BROKER_PORT}/0 + +FLOWER_PORT=8888 +FLOWER_USER=mouse +FLOWER_PASSWORD=changeme + +EMAIL_HOST="smtp.mailgun.org" +EMAIL_PORT=587 +EMAIL_HOST_USER=mail@your.domain.here +EMAIL_HOST_PASSWORD=emailpassword123 +EMAIL_USE_TLS=true +EMAIL_USE_SSL=false + +# Set this to true when initializing certbot for domain, false when not +CERTBOT_INIT=false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..b2cd33f8 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +**/vendor/** diff --git a/.eslintrc.js b/.eslintrc.js index d39859f1..b65fe988 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,5 +6,85 @@ module.exports = { "es6": true }, - "extends": "eslint:recommended" + "extends": "eslint:recommended", + + "rules": { + // Possible Errors + "no-async-promise-executor": "error", + "no-await-in-loop": "error", + "no-class-assign": "error", + "no-confusing-arrow": "error", + "no-const-assign": "error", + "no-dupe-class-members": "error", + "no-duplicate-imports": "error", + "no-template-curly-in-string": "error", + "no-useless-computed-key": "error", + "no-useless-constructor": "error", + "no-useless-rename": "error", + "require-atomic-updates": "error", + + // Best practices + "strict": "error", + "no-var": "error", + + // Stylistic Issues + "arrow-spacing": "error", + "capitalized-comments": [ + "warn", + "always", + { + "ignoreConsecutiveComments": true + }, + ], + "keyword-spacing": "error", + "lines-around-comment": [ + "error", + { + "beforeBlockComment": true, + "beforeLineComment": true, + "allowBlockStart": true, + "allowClassStart": true, + "allowObjectStart": true, + "allowArrayStart": true, + }, + ], + "no-multiple-empty-lines": [ + "error", + { + "max": 1, + }, + ], + "padded-blocks": [ + "error", + "never", + ], + "padding-line-between-statements": [ + "error", + { + // always before return + "blankLine": "always", + "prev": "*", + "next": "return", + }, + { + // always before block-like expressions + "blankLine": "always", + "prev": "*", + "next": "block-like", + }, + { + // always after variable declaration + "blankLine": "always", + "prev": [ "const", "let", "var" ], + "next": "*", + }, + { + // not necessary between variable declaration + "blankLine": "any", + "prev": [ "const", "let", "var" ], + "next": [ "const", "let", "var" ], + }, + ], + "space-before-blocks": "error", + } }; diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 5fc849d6..afa9cf2b 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -8,6 +8,6 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - uses: psf/black@20.8b1 + - uses: psf/black@stable with: args: ". --check -l 80 -S" diff --git a/.github/workflows/lint-frontend.yaml b/.github/workflows/lint-frontend.yaml index 978bbbbe..54cac04d 100644 --- a/.github/workflows/lint-frontend.yaml +++ b/.github/workflows/lint-frontend.yaml @@ -3,12 +3,14 @@ name: Lint Frontend on: push: - branches: [ main, ci ] + branches: [ main, ci, frontend ] paths: - '.github/workflows/**' - 'static/**' + - '.eslintrc' + - '.stylelintrc' pull_request: - branches: [ main, ci ] + branches: [ main, ci, frontend ] jobs: lint: @@ -16,14 +18,21 @@ jobs: runs-on: ubuntu-20.04 steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it. - uses: actions/checkout@v2 - name: Install modules run: yarn + # See .stylelintignore for files that are not linted. - name: Run stylelint - run: yarn stylelint **/static/**/*.css --report-needless-disables --report-invalid-scope-disables + run: > + yarn stylelint bookwyrm/static/**/*.css \ + --report-needless-disables \ + --report-invalid-scope-disables + # See .eslintignore for files that are not linted. - name: Run ESLint - run: yarn eslint . --ext .js,.jsx,.ts,.tsx + run: > + yarn eslint bookwyrm/static \ + --ext .js,.jsx,.ts,.tsx diff --git a/.gitignore b/.gitignore index 71fa61bf..cf88e987 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ #Node tools /node_modules/ + +#nginx +nginx/default.conf diff --git a/.stylelintignore b/.stylelintignore index f456cb22..b2cd33f8 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -1,2 +1 @@ -bookwyrm/static/css/bulma.*.css* -bookwyrm/static/css/icons.css +**/vendor/** diff --git a/README.md b/README.md index e798fedf..161f91b9 100644 --- a/README.md +++ b/README.md @@ -9,29 +9,17 @@ Social reading and reviewing, decentralized with ActivityPub - [What it is and isn't](#what-it-is-and-isnt) - [The role of federation](#the-role-of-federation) - [Features](#features) -- [Setting up the developer environment](#setting-up-the-developer-environment) -- [Installing in Production](#installing-in-production) - [Book data](#book-data) +- [Set up Bookwyrm](#set-up-bookwyrm) ## Joining BookWyrm -BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://github.com/mouse-reeve/bookwyrm/blob/main/instances.md) list. +BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://docs.joinbookwyrm.com/instances.html) list. + +You can request an invite by entering your email address at https://bookwyrm.social. + ## Contributing -There are many ways you can contribute to this project, regardless of your level of technical expertise. - -### Feedback and feature requests -Please feel encouraged and welcome to point out bugs, suggestions, feature requests, and ideas for how things ought to work using [GitHub issues](https://github.com/mouse-reeve/bookwyrm/issues). - -### Code contributions -Code contributions are gladly welcomed! If you're not sure where to start, take a look at the ["Good first issue"](https://github.com/mouse-reeve/bookwyrm/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) tag. Because BookWyrm is a small project, there isn't a lot of formal structure, but there is a huge capacity for one-on-one support, which can look like asking questions as you go, pair programming, video chats, et cetera, so please feel free to reach out. - -If you have questions about the project or contributing, you can set up a video call during BookWyrm ["office hours"](https://calendly.com/mouse-reeve/30min). - -### Translation -Do you speak a language besides English? BookWyrm needs localization! If you're comfortable using git and want to get into the code, there are [instructions](#working-with-translations-and-locale-files) on how to create and edit localization files. If you feel more comfortable working in a regular text editor and would prefer not to run the application, get in touch directly and we can figure out a system, like emailing a text file, that works best. - -### Financial Support -BookWyrm is an ad-free passion project with no intentions of seeking out venture funding or corporate financial relationships. If you want to help keep the project going, you can donate to the [Patreon](https://www.patreon.com/bookwyrm), or make a one time gift via [PayPal](https://paypal.me/oulipo). +See [contributing](https://docs.joinbookwyrm.com/how-to-contribute.html) for code, translation or monetary contributions. ## About BookWyrm ### What it is and isn't @@ -43,7 +31,7 @@ BookWyrm is built on [ActivityPub](http://activitypub.rocks/). With ActivityPub, Federation makes it possible to have small, self-determining communities, in contrast to the monolithic service you find on GoodReads or Twitter. An instance can be focused on a particular interest, be just for a group of friends, or anything else that brings people together. Each community can choose which other instances they want to federate with, and moderate and run their community autonomously. Check out https://runyourown.social/ to get a sense of the philosophy and logistics behind small, high-trust social networks. ### Features -Since the project is still in its early stages, the features are growing every day, and there is plenty of room for suggestions and ideas. Open an [issue](https://github.com/mouse-reeve/bookwyrm/issues) to get the conversation going! +Since the project is still in its early stages, the features are growing every day, and there is plenty of room for suggestions and ideas. Open an [issue](https://github.com/bookwyrm-social/bookwyrm/issues) to get the conversation going! - Posting about books - Compose reviews, with or without ratings, which are aggregated in the book page - Compose other kinds of statuses about books, such as: @@ -73,8 +61,8 @@ Since the project is still in its early stages, the features are growing every d Web backend - [Django](https://www.djangoproject.com/) web server - [PostgreSQL](https://www.postgresql.org/) database -- [ActivityPub](http://activitypub.rocks/) federation -- [Celery](http://celeryproject.org/) task queuing +- [ActivityPub](https://activitypub.rocks/) federation +- [Celery](https://docs.celeryproject.org/) task queuing - [Redis](https://redis.io/) task backend - [Redis (again)](https://redis.io/) activity stream manager @@ -89,246 +77,9 @@ Deployment - [Flower](https://github.com/mher/flower) celery monitoring - [Nginx](https://nginx.org/en/) HTTP server -## Setting up the developer environment - -Set up the environment file: - -``` bash -cp .env.example .env -``` - -For most testing, you'll want to use ngrok. Remember to set the DOMAIN in `.env` to your ngrok domain. - -You'll have to install the Docker and docker-compose. When you're ready, run: - -```bash -docker-compose build -docker-compose run --rm web python manage.py migrate -docker-compose run --rm web python manage.py initdb -docker-compose up -``` - -Once the build is complete, you can access the instance at `localhost:1333` - -### Editing static files -If you edit the CSS or JavaScript, you will need to run Django's `collectstatic` command in order for your changes to have effect. You can do this by running: -``` bash -./bw-dev collectstatic -``` - -### Working with translations and locale files -Text in the html files are wrapped in translation tags (`{% trans %}` and `{% blocktrans %}`), and Django generates locale files for all the strings in which you can add translations for the text. You can find existing translations in the `locale/` directory. - -The application's language is set by a request header sent by your browser to the application, so to change the language of the application, you can change the default language requested by your browser. - -#### Adding a locale -To start translation into a language which is currently supported, run the django-admin `makemessages` command with the language code for the language you want to add (like `de` for German, or `en-gb` for British English): -``` bash -./bw-dev makemessages -l -``` - -#### Editing a locale -When you have a locale file, open the `django.po` in the directory for the language (for example, if you were adding German, `locale/de/LC_MESSAGES/django.po`. All the the text in the application will be shown in paired strings, with `msgid` as the original text, and `msgstr` as the translation (by default, this is set to an empty string, and will display the original text). - -Add your translations to the `msgstr` strings. As the messages in the application are updated, `gettext` will sometimes add best-guess fuzzy matched options for those translations. When a message is marked as fuzzy, it will not be used in the application, so be sure to remove it when you translate that line. - -When you're done, compile the locale by running: - -``` bash -./bw-dev compilemessages -``` - -You can add the `-l ` to only compile one language. When you refresh the application, you should see your translations at work. - -## Installing in Production - -This project is still young and isn't, at the moment, very stable, so please proceed with caution when running in production. - -### Server setup -- Get a domain name and set up DNS for your server -- Set your server up with appropriate firewalls for running a web application (this instruction set is tested against Ubuntu 20.04) -- Set up an email service (such as mailgun) and the appropriate SMTP/DNS settings -- Install Docker and docker-compose - -### Install and configure BookWyrm - -The `production` branch of BookWyrm contains a number of tools not on the `main` branch that are suited for running in production, such as `docker-compose` changes to update the default commands or configuration of containers, and individual changes to container config to enable things like SSL or regular backups. - -Instructions for running BookWyrm in production: - -- Get the application code: - `git clone git@github.com:mouse-reeve/bookwyrm.git` -- Switch to the `production` branch - `git checkout production` -- Create your environment variables file - `cp .env.example .env` - - Add your domain, email address, SMTP credentials - - Set a secure redis password and secret key - - Set a secure database password for postgres -- Update your nginx configuration in `nginx/default.conf` - - Replace `your-domain.com` with your domain name -- Run the application (this should also set up a Certbot ssl cert for your domain) with - `docker-compose up --build`, and make sure all the images build successfully -- When docker has built successfully, stop the process with `CTRL-C` -- Comment out the `command: certonly...` line in `docker-compose.yml` -- Run docker-compose in the background with: `docker-compose up -d` -- Initialize the database with: `./bw-dev initdb` -- Set up schedule backups with cron that runs that `docker-compose exec db pg_dump -U ` and saves the backup to a safe location -- Get the application code: - `git clone git@github.com:mouse-reeve/bookwyrm.git` -- Switch to the `production` branch - `git checkout production` -- Create your environment variables file - `cp .env.example .env` - - Add your domain, email address, SMTP credentials - - Set a secure redis password and secret key - - Set a secure database password for postgres -- Update your nginx configuration in `nginx/default.conf` - - Replace `your-domain.com` with your domain name - - If you aren't using the `www` subdomain, remove the www.your-domain.com version of the domain from the `server_name` in the first server block in `nginx/default.conf` and remove the `-d www.${DOMAIN}` flag at the end of the `certbot` command in `docker-compose.yml`. - - If you are running another web-server on your host machine, you will need to follow the [reverse-proxy instructions](#running-bookwyrm-behind-a-reverse-proxy) -- Run the application (this should also set up a Certbot ssl cert for your domain) with - `docker-compose up --build`, and make sure all the images build successfully - - If you are running other services on your host machine, you may run into errors where services fail when attempting to bind to a port. - See the [troubleshooting guide](#port-conflicts) for advice on resolving this. -- When docker has built successfully, stop the process with `CTRL-C` -- Comment out the `command: certonly...` line in `docker-compose.yml`, and uncomment the following line (`command: renew ...`) so that the certificate will be automatically renewed. -- Uncomment the https redirect and `server` block in `nginx/default.conf` (lines 17-48). -- Run docker-compose in the background with: `docker-compose up -d` -- Initialize the database with: `./bw-dev initdb` - -Congrats! You did it, go to your domain and enjoy the fruits of your labors. - -### Configure your instance -- Register a user account in the application UI -- Make your account a superuser (warning: do *not* use django's `createsuperuser` command) - - On your server, open the django shell - `./bw-dev shell` - - Load your user and make it a superuser - ```python - from bookwyrm import models - user = models.User.objects.get(id=1) - user.is_staff = True - user.is_superuser = True - user.save() - ``` - - Go to the site settings (`/settings/site-settings` on your domain) and configure your instance name, description, code of conduct, and toggle whether registration is open on your instance - -### 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 ` - -### Updating your instance - -When there are changes available in the production branch, you can install and get them running on your instance using the command `./bw-dev update`. This does a number of things: -- `git pull` gets the updated code from the git repository. If there are conflicts, you may need to run `git pull` separately and resolve the conflicts before trying the `./bw-dev update` script again. -- `docker-compose build` rebuilds the images, which ensures that the correct packages are installed. This step takes a long time and is only needed when the dependencies (including pip `requirements.txt` packages) have changed, so you can comment it out if you want a quicker update path and don't mind un-commenting it as needed. -- `docker-compose exec web python manage.py migrate` runs the database migrations in Django -- `docker-compose exec web python manage.py collectstatic --no-input` loads any updated static files (such as the JavaScript and CSS) -- `docker-compose restart` reloads the docker containers - -### Re-building activity streams - -If something goes awry with user timelines, and you want to re-create them en mass, there's a management command for that: -`docker-compose run --rm web python manage.py rebuild_feeds` - -### 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. - ## Book data The application is set up to share book and author data between instances, and get book data from arbitrary outside sources. Right now, the only connector is to OpenLibrary, but other connectors could be written. -There are three concepts in the book data model: -- `Book`, an abstract, high-level concept that could mean either a `Work` or an `Edition`. No data is saved as a `Book`, it serves as shared model for `Work` and `Edition` -- `Work`, the theoretical umbrella concept of a book that encompasses every edition of the book, and -- `Edition`, a concrete, actually published version of a book - -Whenever a user interacts with a book, they are interacting with a specific edition. Every work has a default edition, but the user can select other editions. Reviews aggregated for all editions of a work when you view an edition's page. +## Set up Bookwyrm +The [documentation website](https://docs.joinbookwyrm.com/) has instruction on how to set up Bookwyrm in a [developer environment](https://docs.joinbookwyrm.com/developer-environment.html) or [production](https://docs.joinbookwyrm.com/installing-in-production.html). diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index 35b786f7..bfb22fa3 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -5,11 +5,12 @@ import sys from .base_activity import ActivityEncoder, Signature, naive_parse from .base_activity import Link, Mention from .base_activity import ActivitySerializerError, resolve_remote_id -from .image import Image +from .image import Document, Image from .note import Note, GeneratedNote, Article, Comment, Quotation from .note import Review, Rating from .note import Tombstone from .ordered_collection import OrderedCollection, OrderedCollectionPage +from .ordered_collection import CollectionItem, ListItem, ShelfItem from .ordered_collection import BookList, Shelf from .person import Person, PublicKey from .response import ActivitypubResponse @@ -26,5 +27,5 @@ 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 452f61e0..5349e1dd 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -10,11 +10,11 @@ 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__ @@ -22,7 +22,7 @@ class ActivityEncoder(JSONEncoder): @dataclass class Link: - """ for tagging a book in a status """ + """for tagging a book in a status""" href: str name: str @@ -31,14 +31,14 @@ class Link: @dataclass class Mention(Link): - """ a subtype of Link for mentioning an actor """ + """a subtype of Link for mentioning an actor""" type: str = "Mention" @dataclass class Signature: - """ public key block """ + """public key block""" creator: str created: str @@ -47,15 +47,19 @@ class Signature: 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"): # ugh activity_json["type"] = "PublicKey" + + activity_type = activity_json.get("type") try: - activity_type = activity_json["type"] serializer = activity_objects[activity_type] except KeyError as e: + # we know this exists and that we can't handle it + if activity_type in ["Question"]: + return None raise ActivitySerializerError(e) return serializer(activity_objects=activity_objects, **activity_json) @@ -63,7 +67,7 @@ def naive_parse(activity_objects, activity_json, serializer=None): @dataclass(init=False) class ActivityObject: - """ actor activitypub json """ + """actor activitypub json""" id: str type: str @@ -102,7 +106,7 @@ class ActivityObject: 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 @@ -111,7 +115,7 @@ class ActivityObject: and hasattr(model, "ignore_activity") and model.ignore_activity(self) ): - raise ActivitySerializerError() + return None # check for an existing instance instance = instance or model.find_existing(self.serialize()) @@ -177,7 +181,7 @@ class ActivityObject: 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(): @@ -196,7 +200,7 @@ class ActivityObject: def set_related_field( model_name, origin_model_name, related_field_name, related_remote_id, data ): - """ load reverse related fields (editions, attachments) without blocking """ + """load reverse related fields (editions, attachments) without blocking""" model = apps.get_model("bookwyrm.%s" % model_name, require_ready=True) origin_model = apps.get_model("bookwyrm.%s" % origin_model_name, require_ready=True) @@ -232,7 +236,7 @@ def set_related_field( 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 @@ -251,7 +255,7 @@ def get_model_from_type(activity_type): def resolve_remote_id( remote_id, model=None, refresh=False, save=True, get_activity=False ): - """ take a remote_id and return an instance, creating if necessary """ + """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: diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py index ca4d69da..dfe14b6f 100644 --- a/bookwyrm/activitypub/book.py +++ b/bookwyrm/activitypub/book.py @@ -3,7 +3,7 @@ from dataclasses import dataclass, field from typing import List from .base_activity import ActivityObject -from .image import Image +from .image import Document @dataclass(init=False) @@ -15,11 +15,12 @@ class BookData(ActivityObject): librarythingKey: str = None goodreadsKey: str = None bnfId: str = None + lastEditedBy: str = None @dataclass(init=False) class Book(BookData): - """ serializes an edition or work, abstract """ + """serializes an edition or work, abstract""" title: str sortTitle: str = "" @@ -35,13 +36,13 @@ class Book(BookData): firstPublishedDate: str = "" publishedDate: str = "" - cover: Image = None + cover: Document = None type: str = "Book" @dataclass(init=False) class Edition(Book): - """ Edition instance of a book object """ + """Edition instance of a book object""" work: str isbn10: str = "" @@ -58,7 +59,7 @@ class Edition(Book): @dataclass(init=False) class Work(Book): - """ work instance of a book object """ + """work instance of a book object""" lccn: str = "" defaultEdition: str = "" @@ -68,7 +69,7 @@ class Work(Book): @dataclass(init=False) class Author(BookData): - """ author of a book """ + """author of a book""" name: str isni: str = None diff --git a/bookwyrm/activitypub/image.py b/bookwyrm/activitypub/image.py index 931de977..7950faaf 100644 --- a/bookwyrm/activitypub/image.py +++ b/bookwyrm/activitypub/image.py @@ -4,10 +4,17 @@ from .base_activity import ActivityObject @dataclass(init=False) -class Image(ActivityObject): - """ image block """ +class Document(ActivityObject): + """a document""" url: str name: str = "" type: str = "Document" id: str = None + + +@dataclass(init=False) +class Image(Document): + """an image""" + + type: str = "Image" diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index a739eafa..b501c3d6 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -4,24 +4,24 @@ from typing import Dict, List from django.apps import apps from .base_activity import ActivityObject, Link -from .image import Image +from .image import Document @dataclass(init=False) class Tombstone(ActivityObject): - """ the placeholder for a deleted status """ + """the placeholder for a deleted status""" type: str = "Tombstone" def to_model(self, *args, **kwargs): # pylint: disable=unused-argument - """ this should never really get serialized, just searched for """ + """this should never really get serialized, just searched for""" 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 @@ -32,14 +32,14 @@ class Note(ActivityObject): inReplyTo: str = "" summary: str = "" tag: List[Link] = field(default_factory=lambda: []) - attachment: List[Image] = field(default_factory=lambda: []) + attachment: List[Document] = field(default_factory=lambda: []) sensitive: bool = False type: str = "Note" @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" @@ -47,14 +47,14 @@ class Article(Note): @dataclass(init=False) class GeneratedNote(Note): - """ just a re-typed note """ + """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" @@ -62,7 +62,7 @@ class Comment(Note): @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" @@ -70,7 +70,7 @@ class Quotation(Comment): @dataclass(init=False) class Review(Comment): - """ a full book review """ + """a full book review""" name: str = None rating: int = None @@ -79,7 +79,7 @@ class Review(Comment): @dataclass(init=False) class Rating(Comment): - """ just a star rating """ + """just a star rating""" rating: int content: str = None diff --git a/bookwyrm/activitypub/ordered_collection.py b/bookwyrm/activitypub/ordered_collection.py index 6da60832..e3a83be8 100644 --- a/bookwyrm/activitypub/ordered_collection.py +++ b/bookwyrm/activitypub/ordered_collection.py @@ -7,7 +7,7 @@ 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 @@ -19,7 +19,7 @@ class OrderedCollection(ActivityObject): @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: []) @@ -27,14 +27,14 @@ class OrderedCollectionPrivate(OrderedCollection): @dataclass(init=False) class Shelf(OrderedCollectionPrivate): - """ structure of an ordered collection activity """ + """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" @@ -43,10 +43,37 @@ class BookList(OrderedCollectionPrivate): @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" + + +@dataclass(init=False) +class CollectionItem(ActivityObject): + """an item in a collection""" + + actor: str + type: str = "CollectionItem" + + +@dataclass(init=False) +class ListItem(CollectionItem): + """a book on a list""" + + book: str + notes: str = None + approved: bool = True + order: int = None + type: str = "ListItem" + + +@dataclass(init=False) +class ShelfItem(CollectionItem): + """a book on a list""" + + book: str + type: str = "ShelfItem" diff --git a/bookwyrm/activitypub/person.py b/bookwyrm/activitypub/person.py index 4ab9f08e..d5f37946 100644 --- a/bookwyrm/activitypub/person.py +++ b/bookwyrm/activitypub/person.py @@ -8,7 +8,7 @@ from .image import Image @dataclass(init=False) class PublicKey(ActivityObject): - """ public key block """ + """public key block""" owner: str publicKeyPem: str @@ -17,12 +17,13 @@ class PublicKey(ActivityObject): @dataclass(init=False) class Person(ActivityObject): - """ actor activitypub json """ + """actor activitypub json""" preferredUsername: str inbox: str publicKey: PublicKey followers: str = None + following: str = None outbox: str = None endpoints: Dict = None name: str = None diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index 3686b3f3..f26936d7 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -1,69 +1,83 @@ -""" undo wrapper activity """ +""" activities that do things """ from dataclasses import dataclass, field from typing import List from django.apps import apps from .base_activity import ActivityObject, Signature, resolve_remote_id -from .book import Edition +from .ordered_collection import CollectionItem @dataclass(init=False) class Verb(ActivityObject): - """generic fields for activities - maybe an unecessary level of - abstraction but w/e""" + """generic fields for activities""" actor: str object: ActivityObject def action(self): - """ usually we just want to save, this can be overridden as needed """ - self.object.to_model() + """usually we just want to update and save""" + # self.object may return None if the object is invalid in an expected way + # ie, Question type + if self.object: + self.object.to_model() @dataclass(init=False) class Create(Verb): - """ Create activity """ + """Create activity""" - to: List - cc: List + to: List[str] + cc: List[str] = field(default_factory=lambda: []) signature: Signature = None type: str = "Create" @dataclass(init=False) class Delete(Verb): - """ Create activity """ + """Create activity""" - to: List - cc: List + to: List[str] + cc: List[str] = field(default_factory=lambda: []) type: str = "Delete" def action(self): - """ find and delete the activity object """ - obj = self.object.to_model(save=False, allow_create=False) - obj.delete() + """find and delete the activity object""" + if not self.object: + return + + if isinstance(self.object, str): + # Deleted users are passed as strings. Not wild about this fix + model = apps.get_model("bookwyrm.User") + obj = model.find_existing_by_remote_id(self.object) + else: + obj = self.object.to_model(save=False, allow_create=False) + + if obj: + obj.delete() + # if we can't find it, we don't need to delete it because we don't have it @dataclass(init=False) class Update(Verb): - """ Update activity """ + """Update activity""" - to: List + to: List[str] type: str = "Update" def action(self): - """ update a model instance from the dataclass """ - self.object.to_model(allow_create=False) + """update a model instance from the dataclass""" + if self.object: + self.object.to_model(allow_create=False) @dataclass(init=False) class Undo(Verb): - """ Undo an activity """ + """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 @@ -89,107 +103,98 @@ class Undo(Verb): @dataclass(init=False) class Follow(Verb): - """ Follow activity """ + """Follow activity""" object: str 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" 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" 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" 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 + target: ActivityObject + object: CollectionItem type: str = "Add" - notes: str = None - order: int = 0 - approved: bool = True def action(self): - """ add obj to collection """ - target = resolve_remote_id(self.target, refresh=False) - # 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) + """figure out the target to assign the item to a collection""" + target = resolve_remote_id(self.target) + item = self.object.to_model(save=False) + setattr(item, item.collection_field, target) + item.save() @dataclass(init=False) -class Remove(Verb): - """Remove activity """ +class Remove(Add): + """Remove activity""" - target: ActivityObject type: str = "Remove" def action(self): - """ 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() + """find and remove the activity object""" + obj = self.object.to_model(save=False, allow_create=False) + if obj: + obj.delete() @dataclass(init=False) class Like(Verb): - """ a user faving an object """ + """a user faving an object""" object: str type: str = "Like" def action(self): - """ like """ + """like""" self.to_model() @dataclass(init=False) class Announce(Verb): - """ boosting a status """ + """boosting a status""" published: str to: List[str] = field(default_factory=lambda: []) @@ -198,5 +203,5 @@ class Announce(Verb): type: str = "Announce" def action(self): - """ boost """ + """boost""" self.to_model() diff --git a/bookwyrm/activitystreams.py b/bookwyrm/activitystreams.py index 279079c8..86321cd8 100644 --- a/bookwyrm/activitystreams.py +++ b/bookwyrm/activitystreams.py @@ -1,80 +1,57 @@ """ access the activity streams stored in redis """ -from abc import ABC from django.dispatch import receiver from django.db.models import signals, Q -import redis -from bookwyrm import models, settings +from bookwyrm import models +from bookwyrm.redis_store import RedisStore, r from bookwyrm.views.helpers import privacy_filter -r = redis.Redis( - host=settings.REDIS_ACTIVITY_HOST, port=settings.REDIS_ACTIVITY_PORT, db=0 -) - -class ActivityStream(ABC): - """ a category of activity stream (like home, local, federated) """ +class ActivityStream(RedisStore): + """a category of activity stream (like home, local, federated)""" def stream_id(self, user): - """ the redis key for this user's instance of this stream """ + """the redis key for this user's instance of this stream""" return "{}-{}".format(user.id, self.key) def unread_id(self, user): - """ the redis key for this user's unread count for this stream """ + """the redis key for this user's unread count for this stream""" return "{}-unread".format(self.stream_id(user)) - def get_value(self, status): # pylint: disable=no-self-use - """ the status id and the rank (ie, published date) """ - return {status.id: status.published_date.timestamp()} + def get_rank(self, obj): # pylint: disable=no-self-use + """statuses are sorted by date published""" + return obj.published_date.timestamp() def add_status(self, status): - """ add a status to users' feeds """ - value = self.get_value(status) - # we want to do this as a bulk operation, hence "pipeline" - pipeline = r.pipeline() - for user in self.stream_users(status): - # add the status to the feed - pipeline.zadd(self.stream_id(user), value) - pipeline.zremrangebyrank( - self.stream_id(user), 0, -1 * settings.MAX_STREAM_LENGTH - ) + """add a status to users' feeds""" + # the pipeline contains all the add-to-stream activities + pipeline = self.add_object_to_related_stores(status, execute=False) + + for user in self.get_audience(status): # add to the unread status count pipeline.incr(self.unread_id(user)) + # and go! pipeline.execute() - def remove_status(self, status): - """ remove a status from all feeds """ - pipeline = r.pipeline() - for user in self.stream_users(status): - pipeline.zrem(self.stream_id(user), -1, status.id) - pipeline.execute() - def add_user_statuses(self, viewer, user): - """ add a user's statuses to another user's feed """ - pipeline = r.pipeline() - statuses = user.status_set.all()[: settings.MAX_STREAM_LENGTH] - for status in statuses: - pipeline.zadd(self.stream_id(viewer), self.get_value(status)) - if statuses: - pipeline.zremrangebyrank( - self.stream_id(user), 0, -1 * settings.MAX_STREAM_LENGTH - ) - pipeline.execute() + """add a user's statuses to another user's feed""" + # only add the statuses that the viewer should be able to see (ie, not dms) + statuses = privacy_filter(viewer, user.status_set.all()) + self.bulk_add_objects_to_store(statuses, self.stream_id(viewer)) def remove_user_statuses(self, viewer, user): - """ remove a user's status from another user's feed """ - pipeline = r.pipeline() - for status in user.status_set.all()[: settings.MAX_STREAM_LENGTH]: - pipeline.lrem(self.stream_id(viewer), -1, status.id) - pipeline.execute() + """remove a user's status from another user's feed""" + # remove all so that followers only statuses are removed + statuses = user.status_set.all() + self.bulk_remove_objects_from_store(statuses, self.stream_id(viewer)) def get_activity_stream(self, user): - """ load the ids for statuses to be displayed """ + """load the statuses to be displayed""" # clear unreads for this feed r.set(self.unread_id(user), 0) - statuses = r.zrevrange(self.stream_id(user), 0, -1) + statuses = self.get_store(self.stream_id(user)) return ( models.Status.objects.select_subclasses() .filter(id__in=statuses) @@ -82,27 +59,15 @@ class ActivityStream(ABC): ) def get_unread_count(self, user): - """ get the unread status count for this user's feed """ + """get the unread status count for this user's feed""" return int(r.get(self.unread_id(user)) or 0) - def populate_stream(self, user): - """ go from zero to a timeline """ - pipeline = r.pipeline() - statuses = self.stream_statuses(user) + def populate_streams(self, user): + """go from zero to a timeline""" + self.populate_store(self.stream_id(user)) - stream_id = self.stream_id(user) - for status in statuses.all()[: settings.MAX_STREAM_LENGTH]: - pipeline.zadd(stream_id, self.get_value(status)) - - # only trim the stream if statuses were added - if statuses.exists(): - pipeline.zremrangebyrank( - self.stream_id(user), 0, -1 * settings.MAX_STREAM_LENGTH - ) - pipeline.execute() - - def stream_users(self, status): # pylint: disable=no-self-use - """ given a status, what users should see it """ + def get_audience(self, status): # pylint: disable=no-self-use + """given a status, what users should see it""" # direct messages don't appeard in feeds, direct comments/reviews/etc do if status.privacy == "direct" and status.status_type == "Note": return [] @@ -129,22 +94,29 @@ class ActivityStream(ABC): ) return audience.distinct() - def stream_statuses(self, user): # pylint: disable=no-self-use - """ given a user, what statuses should they see on this stream """ + def get_stores_for_object(self, obj): + return [self.stream_id(u) for u in self.get_audience(obj)] + + def get_statuses_for_user(self, user): # pylint: disable=no-self-use + """given a user, what statuses should they see on this stream""" return privacy_filter( user, models.Status.objects.select_subclasses(), privacy_levels=["public", "unlisted", "followers"], ) + def get_objects_for_store(self, store): + user = models.User.objects.get(id=store.split("-")[0]) + return self.get_statuses_for_user(user) + class HomeStream(ActivityStream): - """ users you follow """ + """users you follow""" key = "home" - def stream_users(self, status): - audience = super().stream_users(status) + def get_audience(self, status): + audience = super().get_audience(status) if not audience: return [] return audience.filter( @@ -152,7 +124,7 @@ class HomeStream(ActivityStream): | Q(following=status.user) # if the user is following the author ).distinct() - def stream_statuses(self, user): + def get_statuses_for_user(self, user): return privacy_filter( user, models.Status.objects.select_subclasses(), @@ -162,17 +134,17 @@ class HomeStream(ActivityStream): class LocalStream(ActivityStream): - """ users you follow """ + """users you follow""" key = "local" - def stream_users(self, status): + def get_audience(self, status): # this stream wants no part in non-public statuses if status.privacy != "public" or not status.user.local: return [] - return super().stream_users(status) + return super().get_audience(status) - def stream_statuses(self, user): + def get_statuses_for_user(self, user): # all public statuses by a local user return privacy_filter( user, @@ -182,17 +154,17 @@ class LocalStream(ActivityStream): class FederatedStream(ActivityStream): - """ users you follow """ + """users you follow""" key = "federated" - def stream_users(self, status): + def get_audience(self, status): # this stream wants no part in non-public statuses if status.privacy != "public": return [] - return super().stream_users(status) + return super().get_audience(status) - def stream_statuses(self, user): + def get_statuses_for_user(self, user): return privacy_filter( user, models.Status.objects.select_subclasses(), @@ -210,14 +182,14 @@ streams = { @receiver(signals.post_save) # pylint: disable=unused-argument def add_status_on_create(sender, instance, created, *args, **kwargs): - """ add newly created statuses to activity feeds """ + """add newly created statuses to activity feeds""" # we're only interested in new statuses if not issubclass(sender, models.Status): return if instance.deleted: for stream in streams.values(): - stream.remove_status(instance) + stream.remove_object_from_related_stores(instance) return if not created: @@ -231,16 +203,16 @@ def add_status_on_create(sender, instance, created, *args, **kwargs): @receiver(signals.post_delete, sender=models.Boost) # pylint: disable=unused-argument def remove_boost_on_delete(sender, instance, *args, **kwargs): - """ boosts are deleted """ + """boosts are deleted""" # we're only interested in new statuses for stream in streams.values(): - stream.remove_status(instance) + stream.remove_object_from_related_stores(instance) @receiver(signals.post_save, sender=models.UserFollows) # pylint: disable=unused-argument def add_statuses_on_follow(sender, instance, created, *args, **kwargs): - """ add a newly followed user's statuses to feeds """ + """add a newly followed user's statuses to feeds""" if not created or not instance.user_subject.local: return HomeStream().add_user_statuses(instance.user_subject, instance.user_object) @@ -249,7 +221,7 @@ def add_statuses_on_follow(sender, instance, created, *args, **kwargs): @receiver(signals.post_delete, sender=models.UserFollows) # pylint: disable=unused-argument def remove_statuses_on_unfollow(sender, instance, *args, **kwargs): - """ remove statuses from a feed on unfollow """ + """remove statuses from a feed on unfollow""" if not instance.user_subject.local: return HomeStream().remove_user_statuses(instance.user_subject, instance.user_object) @@ -258,7 +230,7 @@ def remove_statuses_on_unfollow(sender, instance, *args, **kwargs): @receiver(signals.post_save, sender=models.UserBlocks) # pylint: disable=unused-argument def remove_statuses_on_block(sender, instance, *args, **kwargs): - """ remove statuses from all feeds on block """ + """remove statuses from all feeds on block""" # blocks apply ot all feeds if instance.user_subject.local: for stream in streams.values(): @@ -273,7 +245,7 @@ def remove_statuses_on_block(sender, instance, *args, **kwargs): @receiver(signals.post_delete, sender=models.UserBlocks) # pylint: disable=unused-argument def add_statuses_on_unblock(sender, instance, *args, **kwargs): - """ remove statuses from all feeds on block """ + """remove statuses from all feeds on block""" public_streams = [LocalStream(), FederatedStream()] # add statuses back to streams with statuses from anyone if instance.user_subject.local: @@ -289,9 +261,9 @@ def add_statuses_on_unblock(sender, instance, *args, **kwargs): @receiver(signals.post_save, sender=models.User) # pylint: disable=unused-argument def populate_streams_on_account_create(sender, instance, created, *args, **kwargs): - """ build a user's feeds when they join """ + """build a user's feeds when they join""" if not created or not instance.local: return for stream in streams.values(): - stream.populate_stream(instance) + stream.populate_streams(instance) diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 43cd6aad..14fe3cb7 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -16,7 +16,7 @@ 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 @@ -39,7 +39,7 @@ class AbstractMinimalConnector(ABC): 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 @@ -55,7 +55,7 @@ class AbstractMinimalConnector(ABC): return results def isbn_search(self, query): - """ isbn search """ + """isbn search""" params = {} data = self.get_search_data( "%s%s" % (self.isbn_search_url, query), @@ -74,27 +74,27 @@ class AbstractMinimalConnector(ABC): @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) @@ -103,14 +103,14 @@ class AbstractConnector(AbstractMinimalConnector): 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 @@ -159,7 +159,7 @@ class AbstractConnector(AbstractMinimalConnector): return get_data(remote_id) 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 edition_activity = activitypub.Edition(**mapped_data) @@ -179,7 +179,7 @@ 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 @@ -187,29 +187,33 @@ class AbstractConnector(AbstractMinimalConnector): data = self.get_book_data(remote_id) mapped_data = dict_from_mappings(data, self.author_mappings) - activity = activitypub.Author(**mapped_data) + try: + activity = activitypub.Author(**mapped_data) + except activitypub.ActivitySerializerError: + return None + # this will dedupe return activity.to_model(model=models.Author) @abstractmethod def is_work_data(self, data): - """ differentiate works and editions """ + """differentiate works and editions""" @abstractmethod def get_edition_from_work_data(self, data): - """ every work needs at least one edition """ + """every work needs at least one edition""" @abstractmethod def get_work_from_edition_data(self, data): - """ every edition needs a work """ + """every edition needs a work""" @abstractmethod def get_authors_from_data(self, data): - """ load author data """ + """load author data""" @abstractmethod def expand_book_data(self, book): - """ get more info on a book """ + """get more info on a book""" def dict_from_mappings(data, mappings): @@ -222,7 +226,13 @@ def dict_from_mappings(data, mappings): def get_data(url, params=None): - """ wrapper for request.get """ + """wrapper for request.get""" + # check if the url is blocked + if models.FederatedServer.is_blocked(url): + raise ConnectorException( + "Attempting to load data from blocked url: {:s}".format(url) + ) + try: resp = requests.get( url, @@ -248,7 +258,7 @@ def get_data(url, params=None): def get_image(url): - """ wrapper for requesting an image """ + """wrapper for requesting an image""" try: resp = requests.get( url, @@ -266,7 +276,7 @@ def get_image(url): @dataclass class SearchResult: - """ standardized search result object """ + """standardized search result object""" title: str key: str @@ -283,14 +293,14 @@ class SearchResult: ) def json(self): - """ serialize a connector for json response """ + """serialize a connector for json response""" serialized = asdict(self) 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 @@ -300,7 +310,7 @@ 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 diff --git a/bookwyrm/connectors/bookwyrm_connector.py b/bookwyrm/connectors/bookwyrm_connector.py index f7869d55..640a0bca 100644 --- a/bookwyrm/connectors/bookwyrm_connector.py +++ b/bookwyrm/connectors/bookwyrm_connector.py @@ -4,7 +4,7 @@ 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) diff --git a/bookwyrm/connectors/connector_manager.py b/bookwyrm/connectors/connector_manager.py index caf6bcbe..3a6bf13c 100644 --- a/bookwyrm/connectors/connector_manager.py +++ b/bookwyrm/connectors/connector_manager.py @@ -1,5 +1,6 @@ """ interface with whatever connectors the app has """ import importlib +import logging import re from urllib.parse import urlparse @@ -11,13 +12,15 @@ from requests import HTTPError from bookwyrm import models from bookwyrm.tasks import app +logger = logging.getLogger(__name__) + class ConnectorException(HTTPError): - """ when the connector can't do what was asked """ + """when the connector can't do what was asked""" def search(query, min_confidence=0.1): - """ find books based on arbitary keywords """ + """find books based on arbitary keywords""" if not query: return [] results = [] @@ -37,14 +40,17 @@ def search(query, min_confidence=0.1): else: try: result_set = connector.isbn_search(isbn) - except (HTTPError, ConnectorException): - pass + except Exception as e: # pylint: disable=broad-except + logger.exception(e) + continue # if no isbn search or results, we fallback to generic search if result_set in (None, []): try: result_set = connector.search(query, min_confidence=min_confidence) - except (HTTPError, ConnectorException): + except Exception as e: # pylint: disable=broad-except + # we don't want *any* error to crash the whole search page + logger.exception(e) continue # if the search results look the same, ignore them @@ -61,20 +67,22 @@ def search(query, min_confidence=0.1): return results -def local_search(query, min_confidence=0.1, raw=False): - """ only look at local search results """ +def local_search(query, min_confidence=0.1, raw=False, filters=None): + """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) + return connector.search( + query, min_confidence=min_confidence, raw=raw, filters=filters + ) 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: @@ -83,13 +91,13 @@ def first_search_result(query, min_confidence=0.1): def get_connectors(): - """ load all connectors """ + """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 object's server """ + """get the connector related to the object's server""" url = urlparse(remote_id) identifier = url.netloc if not identifier: @@ -113,7 +121,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) @@ -121,7 +129,7 @@ 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 ) @@ -131,6 +139,6 @@ def load_connector(connector_info): @receiver(signals.post_save, sender="bookwyrm.FederatedServer") # pylint: disable=unused-argument def create_connector(sender, instance, created, *args, **kwargs): - """ create a connector to an external bookwyrm server """ + """create a connector to an external bookwyrm server""" if instance.application_type == "bookwyrm": get_or_create_connector("https://{:s}".format(instance.server_name)) diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py index 466bf1e5..69d498b8 100644 --- a/bookwyrm/connectors/openlibrary.py +++ b/bookwyrm/connectors/openlibrary.py @@ -9,7 +9,7 @@ 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) @@ -59,7 +59,7 @@ class Connector(AbstractConnector): ] 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"] except KeyError: @@ -87,16 +87,19 @@ class Connector(AbstractConnector): return get_data(url) def get_authors_from_data(self, data): - """ parse author json and load or create authors """ + """parse author json and load or create authors""" for author_blob in data.get("authors", []): author_blob = author_blob.get("author", author_blob) # this id is "/authors/OL1234567A" author_id = author_blob["key"] url = "%s%s" % (self.base_url, author_id) - yield self.get_or_create_author(url) + author = self.get_or_create_author(url) + if not author: + continue + yield author def get_cover_url(self, cover_blob, size="L"): - """ ask openlibrary for the cover """ + """ask openlibrary for the cover""" if not cover_blob: return None cover_id = cover_blob[0] @@ -138,7 +141,7 @@ class Connector(AbstractConnector): ) def load_edition_data(self, olkey): - """ query openlibrary for editions of a work """ + """query openlibrary for editions of a work""" url = "%s/works/%s/editions" % (self.books_url, olkey) return get_data(url) @@ -163,7 +166,7 @@ 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"): return False @@ -182,19 +185,19 @@ def ignore_edition(edition_data): 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 def get_openlibrary_key(key): - """ convert /books/OL27320736M into OL27320736M """ + """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)) @@ -202,7 +205,7 @@ def get_languages(language_blob): 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: diff --git a/bookwyrm/connectors/self_connector.py b/bookwyrm/connectors/self_connector.py index 500ffd74..0dc922a5 100644 --- a/bookwyrm/connectors/self_connector.py +++ b/bookwyrm/connectors/self_connector.py @@ -10,18 +10,19 @@ 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 """ + def search(self, query, min_confidence=0.1, raw=False, filters=None): + """search your local database""" + filters = filters or [] if not query: return [] # first, try searching unqiue identifiers - results = search_identifiers(query) + results = search_identifiers(query, *filters) if not results: # then try searching title/author - results = search_title_author(query, min_confidence) + results = search_title_author(query, min_confidence, *filters) search_results = [] for result in results: if raw: @@ -35,7 +36,7 @@ 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 [] @@ -87,26 +88,26 @@ 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): pass -def search_identifiers(query): - """ tries remote_id, isbn; defined as dedupe fields on the model """ - filters = [ +def search_identifiers(query, *filters): + """tries remote_id, isbn; defined as dedupe fields on the model""" + or_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)) + *filters, reduce(operator.or_, (Q(**f) for f in or_filters)) ).distinct() # when there are multiple editions of the same work, pick the default. @@ -114,8 +115,8 @@ def search_identifiers(query): return results.filter(parent_work__default_edition__id=F("id")) or results -def search_title_author(query, min_confidence): - """ searches for title and author """ +def search_title_author(query, min_confidence, *filters): + """searches for title and author""" vector = ( SearchVector("title", weight="A") + SearchVector("subtitle", weight="B") @@ -126,7 +127,7 @@ def search_title_author(query, min_confidence): results = ( models.Edition.objects.annotate(search=vector) .annotate(rank=SearchRank(vector, query)) - .filter(rank__gt=min_confidence) + .filter(*filters, rank__gt=min_confidence) .order_by("-rank") ) diff --git a/bookwyrm/context_processors.py b/bookwyrm/context_processors.py index 8f79a652..f5f25186 100644 --- a/bookwyrm/context_processors.py +++ b/bookwyrm/context_processors.py @@ -3,5 +3,5 @@ from bookwyrm import models def site_settings(request): # pylint: disable=unused-argument - """ include the custom info about the site """ + """include the custom info about the site""" return {"site": models.SiteSettings.objects.get()} diff --git a/bookwyrm/emailing.py b/bookwyrm/emailing.py index 1804254b..657310b0 100644 --- a/bookwyrm/emailing.py +++ b/bookwyrm/emailing.py @@ -8,7 +8,7 @@ from bookwyrm.settings import DOMAIN def email_data(): - """ fields every email needs """ + """fields every email needs""" site = models.SiteSettings.objects.get() if site.logo_small: logo_path = "/images/{}".format(site.logo_small.url) @@ -24,14 +24,14 @@ def email_data(): def invite_email(invite_request): - """ send out an invite code """ + """send out an invite code""" data = email_data() data["invite_link"] = invite_request.invite.link send_email.delay(invite_request.email, *format_email("invite", data)) def password_reset_email(reset_code): - """ generate a password reset email """ + """generate a password reset email""" data = email_data() data["reset_link"] = reset_code.link data["user"] = reset_code.user.display_name @@ -39,7 +39,7 @@ def password_reset_email(reset_code): def format_email(email_name, data): - """ render the email templates """ + """render the email templates""" subject = ( get_template("email/{}/subject.html".format(email_name)).render(data).strip() ) @@ -58,7 +58,7 @@ def format_email(email_name, data): @app.task def send_email(recipient, subject, html_content, text_content): - """ use a task to send the email """ + """use a task to send the email""" email = EmailMultiAlternatives( subject, text_content, settings.DEFAULT_FROM_EMAIL, [recipient] ) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index b159a89e..b6197f33 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -3,7 +3,7 @@ import datetime from collections import defaultdict from django import forms -from django.forms import ModelForm, PasswordInput, widgets +from django.forms import ModelForm, PasswordInput, widgets, ChoiceField from django.forms.widgets import Textarea from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -12,7 +12,7 @@ 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: "") @@ -150,12 +150,10 @@ class LimitedEditUserForm(CustomForm): help_texts = {f: None for f in fields} -class TagForm(CustomForm): +class UserGroupForm(CustomForm): class Meta: - model = models.Tag - fields = ["name"] - help_texts = {f: None for f in fields} - labels = {"name": "Add a tag"} + model = models.User + fields = ["groups"] class CoverForm(CustomForm): @@ -200,7 +198,7 @@ class ImportForm(forms.Form): 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": @@ -219,7 +217,7 @@ class ExpiryWidget(widgets.Select): class InviteRequestForm(CustomForm): def clean(self): - """ make sure the email isn't in use by a registered user """ + """make sure the email isn't in use by a registered user""" cleaned_data = super().clean() email = cleaned_data.get("email") if email and models.User.objects.filter(email=email).exists(): @@ -281,3 +279,26 @@ class ReportForm(CustomForm): class Meta: model = models.Report fields = ["user", "reporter", "statuses", "note"] + + +class ServerForm(CustomForm): + class Meta: + model = models.FederatedServer + exclude = ["remote_id"] + + +class SortListForm(forms.Form): + sort_by = ChoiceField( + choices=( + ("order", _("List Order")), + ("title", _("Book Title")), + ("rating", _("Rating")), + ), + label=_("Sort By"), + ) + direction = ChoiceField( + choices=( + ("ascending", _("Ascending")), + ("descending", _("Descending")), + ), + ) diff --git a/bookwyrm/importers/goodreads_import.py b/bookwyrm/importers/goodreads_import.py index 0b126c14..7b577ea8 100644 --- a/bookwyrm/importers/goodreads_import.py +++ b/bookwyrm/importers/goodreads_import.py @@ -9,7 +9,7 @@ class GoodreadsImporter(Importer): service = "GoodReads" def parse_fields(self, entry): - """ handle the specific fields in goodreads csvs """ + """handle the specific fields in goodreads csvs""" entry.update({"import_source": self.service}) # add missing 'Date Started' field entry.update({"Date Started": None}) diff --git a/bookwyrm/importers/importer.py b/bookwyrm/importers/importer.py index ddbfa304..89c62e73 100644 --- a/bookwyrm/importers/importer.py +++ b/bookwyrm/importers/importer.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) class Importer: - """ Generic class for csv data import from an outside service """ + """Generic class for csv data import from an outside service""" service = "Unknown" delimiter = "," @@ -18,7 +18,7 @@ class Importer: 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 ) @@ -32,16 +32,16 @@ class Importer: return job def save_item(self, job, index, data): # pylint: disable=no-self-use - """ creates and saves an import item """ + """creates and saves an import item""" ImportItem(job=job, index=index, data=data).save() def parse_fields(self, entry): - """ updates csv data with additional info """ + """updates csv data with additional info""" entry.update({"import_source": self.service}) return entry def create_retry_job(self, user, original_job, items): - """ retry items that didn't import """ + """retry items that didn't import""" job = ImportJob.objects.create( user=user, include_reviews=original_job.include_reviews, @@ -53,7 +53,7 @@ class Importer: 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() @@ -61,7 +61,7 @@ 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(): @@ -89,7 +89,7 @@ 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: @@ -116,24 +116,33 @@ def handle_imported_book(source, user, item, include_reviews, privacy): 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 "" - ) - # we don't know the publication date of the review, # but "now" is a bad guess published_date_guess = item.date_read or item.date_added - models.Review.objects.create( - user=user, - book=item.book, - name=review_title, - content=item.review, - rating=item.rating, - published_date=published_date_guess, - privacy=privacy, - ) + if item.review: + review_title = ( + "Review of {!r} on {!r}".format( + item.book.title, + source, + ) + if item.review + else "" + ) + models.Review.objects.create( + user=user, + book=item.book, + name=review_title, + content=item.review, + rating=item.rating, + published_date=published_date_guess, + privacy=privacy, + ) + else: + # just a rating + models.ReviewRating.objects.create( + user=user, + book=item.book, + rating=item.rating, + published_date=published_date_guess, + privacy=privacy, + ) diff --git a/bookwyrm/importers/librarything_import.py b/bookwyrm/importers/librarything_import.py index 3755cb1a..b3175a82 100644 --- a/bookwyrm/importers/librarything_import.py +++ b/bookwyrm/importers/librarything_import.py @@ -6,7 +6,7 @@ from . import Importer class LibrarythingImporter(Importer): - """ csv downloads from librarything """ + """csv downloads from librarything""" service = "LibraryThing" delimiter = "\t" @@ -15,7 +15,7 @@ class LibrarythingImporter(Importer): mandatory_fields = ["Title", "Primary Author"] def parse_fields(self, entry): - """ custom parsing for librarything """ + """custom parsing for librarything""" data = {} data["import_source"] = self.service data["Book Id"] = entry["Book Id"] diff --git a/bookwyrm/management/commands/deduplicate_book_data.py b/bookwyrm/management/commands/deduplicate_book_data.py index edd91a71..ed01a784 100644 --- a/bookwyrm/management/commands/deduplicate_book_data.py +++ b/bookwyrm/management/commands/deduplicate_book_data.py @@ -6,7 +6,7 @@ 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 @@ -24,7 +24,7 @@ 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"): continue @@ -38,7 +38,7 @@ def copy_data(canonical, obj): 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 @@ -68,12 +68,12 @@ def dedupe_model(model): class Command(BaseCommand): - """ dedplucate allllll the book data models """ + """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/erase_streams.py b/bookwyrm/management/commands/erase_streams.py index 042e857f..1d34b1bb 100644 --- a/bookwyrm/management/commands/erase_streams.py +++ b/bookwyrm/management/commands/erase_streams.py @@ -10,15 +10,15 @@ r = redis.Redis( def erase_streams(): - """ throw the whole redis away """ + """throw the whole redis away""" r.flushall() class Command(BaseCommand): - """ delete activity streams for all users """ + """delete activity streams for all users""" help = "Delete all the user streams" # pylint: disable=no-self-use,unused-argument def handle(self, *args, **options): - """ flush all, baby """ + """flush all, baby""" erase_streams() diff --git a/bookwyrm/management/commands/initdb.py b/bookwyrm/management/commands/initdb.py index 45c81089..07071a2e 100644 --- a/bookwyrm/management/commands/initdb.py +++ b/bookwyrm/management/commands/initdb.py @@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand, CommandError from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType -from bookwyrm.models import Connector, SiteSettings, User +from bookwyrm.models import Connector, FederatedServer, SiteSettings, User from bookwyrm.settings import DOMAIN @@ -119,6 +119,16 @@ def init_connectors(): ) +def init_federated_servers(): + """big no to nazis""" + built_in_blocks = ["gab.ai", "gab.com"] + for server in built_in_blocks: + FederatedServer.objects.create( + server_name=server, + status="blocked", + ) + + def init_settings(): SiteSettings.objects.create() @@ -130,4 +140,5 @@ class Command(BaseCommand): init_groups() init_permissions() init_connectors() + init_federated_servers() init_settings() diff --git a/bookwyrm/management/commands/populate_streams.py b/bookwyrm/management/commands/populate_streams.py index 06ca5f07..04f6bf6e 100644 --- a/bookwyrm/management/commands/populate_streams.py +++ b/bookwyrm/management/commands/populate_streams.py @@ -10,21 +10,21 @@ r = redis.Redis( def populate_streams(): - """ build all the streams for all the users """ + """build all the streams for all the users""" users = models.User.objects.filter( local=True, is_active=True, ) for user in users: for stream in activitystreams.streams.values(): - stream.populate_stream(user) + stream.populate_streams(user) class Command(BaseCommand): - """ start all over with user streams """ + """start all over with user streams""" help = "Populate streams for all users" # pylint: disable=no-self-use,unused-argument def handle(self, *args, **options): - """ run feed builder """ + """run feed builder""" populate_streams() diff --git a/bookwyrm/management/commands/remove_editions.py b/bookwyrm/management/commands/remove_editions.py index 6829c6d1..9eb9b7da 100644 --- a/bookwyrm/management/commands/remove_editions.py +++ b/bookwyrm/management/commands/remove_editions.py @@ -5,7 +5,7 @@ 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 @@ -33,10 +33,10 @@ def remove_editions(): class Command(BaseCommand): - """ dedplucate allllll the book data models """ + """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/0046_reviewrating.py b/bookwyrm/migrations/0046_reviewrating.py index 8d149004..26f6f36a 100644 --- a/bookwyrm/migrations/0046_reviewrating.py +++ b/bookwyrm/migrations/0046_reviewrating.py @@ -8,7 +8,7 @@ from psycopg2.extras import execute_values def convert_review_rating(app_registry, schema_editor): - """ take rating type Reviews and convert them to ReviewRatings """ + """take rating type Reviews and convert them to ReviewRatings""" db_alias = schema_editor.connection.alias reviews = ( @@ -29,7 +29,7 @@ VALUES %s""", def unconvert_review_rating(app_registry, schema_editor): - """ undo the conversion from ratings back to reviews""" + """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 diff --git a/bookwyrm/migrations/0062_auto_20210407_1545.py b/bookwyrm/migrations/0062_auto_20210407_1545.py new file mode 100644 index 00000000..3a156637 --- /dev/null +++ b/bookwyrm/migrations/0062_auto_20210407_1545.py @@ -0,0 +1,33 @@ +# Generated by Django 3.1.6 on 2021-04-07 15:45 + +import bookwyrm.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0061_auto_20210402_1435"), + ] + + operations = [ + migrations.AlterField( + model_name="book", + name="series", + field=bookwyrm.models.fields.TextField( + blank=True, max_length=255, null=True + ), + ), + migrations.AlterField( + model_name="book", + name="subtitle", + field=bookwyrm.models.fields.TextField( + blank=True, max_length=255, null=True + ), + ), + migrations.AlterField( + model_name="book", + name="title", + field=bookwyrm.models.fields.TextField(max_length=255), + ), + ] diff --git a/bookwyrm/migrations/0063_auto_20210407_1827.py b/bookwyrm/migrations/0063_auto_20210407_1827.py new file mode 100644 index 00000000..0bd0f2ae --- /dev/null +++ b/bookwyrm/migrations/0063_auto_20210407_1827.py @@ -0,0 +1,37 @@ +# Generated by Django 3.1.6 on 2021-04-07 18:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0062_auto_20210407_1545"), + ] + + operations = [ + migrations.AddField( + model_name="federatedserver", + name="notes", + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name="federatedserver", + name="application_type", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="federatedserver", + name="application_version", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="federatedserver", + name="status", + field=models.CharField( + choices=[("federated", "Federated"), ("blocked", "Blocked")], + default="federated", + max_length=255, + ), + ), + ] diff --git a/bookwyrm/migrations/0063_auto_20210408_1556.py b/bookwyrm/migrations/0063_auto_20210408_1556.py new file mode 100644 index 00000000..750997fb --- /dev/null +++ b/bookwyrm/migrations/0063_auto_20210408_1556.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1.6 on 2021-04-08 15:56 + +import bookwyrm.models.fields +import django.contrib.postgres.fields.citext +import django.contrib.postgres.operations +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0062_auto_20210407_1545"), + ] + + operations = [ + django.contrib.postgres.operations.CITextExtension(), + migrations.AlterField( + model_name="user", + name="localname", + field=django.contrib.postgres.fields.citext.CICharField( + max_length=255, + null=True, + unique=True, + validators=[bookwyrm.models.fields.validate_localname], + ), + ), + ] diff --git a/bookwyrm/migrations/0064_auto_20210408_2208.py b/bookwyrm/migrations/0064_auto_20210408_2208.py new file mode 100644 index 00000000..84a1a128 --- /dev/null +++ b/bookwyrm/migrations/0064_auto_20210408_2208.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1.6 on 2021-04-08 22:08 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0063_auto_20210408_1556"), + ] + + operations = [ + migrations.AlterField( + model_name="listitem", + name="book_list", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="bookwyrm.list" + ), + ), + migrations.AlterField( + model_name="shelfbook", + name="shelf", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="bookwyrm.shelf" + ), + ), + ] diff --git a/bookwyrm/migrations/0064_merge_20210410_1633.py b/bookwyrm/migrations/0064_merge_20210410_1633.py new file mode 100644 index 00000000..77ad541e --- /dev/null +++ b/bookwyrm/migrations/0064_merge_20210410_1633.py @@ -0,0 +1,13 @@ +# Generated by Django 3.1.8 on 2021-04-10 16:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0063_auto_20210408_1556"), + ("bookwyrm", "0063_auto_20210407_1827"), + ] + + operations = [] diff --git a/bookwyrm/migrations/0065_merge_20210411_1702.py b/bookwyrm/migrations/0065_merge_20210411_1702.py new file mode 100644 index 00000000..2bdc425d --- /dev/null +++ b/bookwyrm/migrations/0065_merge_20210411_1702.py @@ -0,0 +1,13 @@ +# Generated by Django 3.1.8 on 2021-04-11 17:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0064_auto_20210408_2208"), + ("bookwyrm", "0064_merge_20210410_1633"), + ] + + operations = [] diff --git a/bookwyrm/migrations/0066_user_deactivation_reason.py b/bookwyrm/migrations/0066_user_deactivation_reason.py new file mode 100644 index 00000000..bb3173a7 --- /dev/null +++ b/bookwyrm/migrations/0066_user_deactivation_reason.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1.8 on 2021-04-12 15:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0065_merge_20210411_1702"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="deactivation_reason", + field=models.CharField( + blank=True, + choices=[ + ("self_deletion", "Self Deletion"), + ("moderator_deletion", "Moderator Deletion"), + ("domain_block", "Domain Block"), + ], + max_length=255, + null=True, + ), + ), + ] diff --git a/bookwyrm/migrations/0067_denullify_list_item_order.py b/bookwyrm/migrations/0067_denullify_list_item_order.py new file mode 100644 index 00000000..51e28371 --- /dev/null +++ b/bookwyrm/migrations/0067_denullify_list_item_order.py @@ -0,0 +1,30 @@ +from django.db import migrations + + +def forwards_func(apps, schema_editor): + # Set all values for ListItem.order + BookList = apps.get_model("bookwyrm", "List") + db_alias = schema_editor.connection.alias + for book_list in BookList.objects.using(db_alias).all(): + for i, item in enumerate(book_list.listitem_set.order_by("id"), 1): + item.order = i + item.save() + + +def reverse_func(apps, schema_editor): + # null all values for ListItem.order + BookList = apps.get_model("bookwyrm", "List") + db_alias = schema_editor.connection.alias + for book_list in BookList.objects.using(db_alias).all(): + for item in book_list.listitem_set.order_by("id"): + item.order = None + item.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0066_user_deactivation_reason"), + ] + + operations = [migrations.RunPython(forwards_func, reverse_func)] diff --git a/bookwyrm/migrations/0068_ordering_for_list_items.py b/bookwyrm/migrations/0068_ordering_for_list_items.py new file mode 100644 index 00000000..fa64f13c --- /dev/null +++ b/bookwyrm/migrations/0068_ordering_for_list_items.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.6 on 2021-04-08 16:15 + +import bookwyrm.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0067_denullify_list_item_order"), + ] + + operations = [ + migrations.AlterField( + model_name="listitem", + name="order", + field=bookwyrm.models.fields.IntegerField(), + ), + migrations.AlterUniqueTogether( + name="listitem", + unique_together={("order", "book_list"), ("book", "book_list")}, + ), + ] diff --git a/bookwyrm/migrations/0069_auto_20210422_1604.py b/bookwyrm/migrations/0069_auto_20210422_1604.py new file mode 100644 index 00000000..6591e7b9 --- /dev/null +++ b/bookwyrm/migrations/0069_auto_20210422_1604.py @@ -0,0 +1,34 @@ +# Generated by Django 3.1.8 on 2021-04-22 16:04 + +import bookwyrm.models.fields +from django.conf import settings +from django.db import migrations +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0068_ordering_for_list_items"), + ] + + operations = [ + migrations.AlterField( + model_name="author", + name="last_edited_by", + field=bookwyrm.models.fields.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="book", + name="last_edited_by", + field=bookwyrm.models.fields.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/bookwyrm/migrations/0070_auto_20210423_0121.py b/bookwyrm/migrations/0070_auto_20210423_0121.py new file mode 100644 index 00000000..0b04c3ca --- /dev/null +++ b/bookwyrm/migrations/0070_auto_20210423_0121.py @@ -0,0 +1,35 @@ +# Generated by Django 3.1.8 on 2021-04-23 01:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0069_auto_20210422_1604"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="usertag", + unique_together=None, + ), + migrations.RemoveField( + model_name="usertag", + name="book", + ), + migrations.RemoveField( + model_name="usertag", + name="tag", + ), + migrations.RemoveField( + model_name="usertag", + name="user", + ), + migrations.DeleteModel( + name="Tag", + ), + migrations.DeleteModel( + name="UserTag", + ), + ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 35e32c2c..2a25a525 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -17,8 +17,6 @@ from .favorite import Favorite from .notification import Notification from .readthrough import ReadThrough, ProgressUpdate, ProgressMode -from .tag import Tag, UserTag - from .user import User, KeyPair, AnnualGoal from .relationship import UserFollows, UserFollowRequest, UserBlocks from .report import Report, ReportComment diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py index a253207a..83b4c0ab 100644 --- a/bookwyrm/models/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -1,5 +1,6 @@ """ activitypub model functionality """ from base64 import b64encode +from collections import namedtuple from functools import reduce import json import operator @@ -25,14 +26,23 @@ from bookwyrm.models.fields import ImageField, ManyToManyField 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! + +PropertyField = namedtuple("PropertyField", ("set_activity_from_field")) + + +def set_activity_from_property_field(activity, obj, field): + """assign a model property value to the activity json""" + activity[field[1]] = getattr(obj, field[0]) + + 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" @@ -52,6 +62,12 @@ class ActivitypubMixin: self.activity_fields = ( self.image_fields + self.many_to_many_fields + self.simple_fields ) + if hasattr(self, "property_fields"): + self.activity_fields += [ + # pylint: disable=cell-var-from-loop + PropertyField(lambda a, o: set_activity_from_property_field(a, o, f)) + for f in self.property_fields + ] # these are separate to avoid infinite recursion issues self.deserialize_reverse_fields = ( @@ -69,7 +85,7 @@ class ActivitypubMixin: @classmethod def find_existing_by_remote_id(cls, remote_id): - """ look up a remote id in the db """ + """look up a remote id in the db""" return cls.find_existing({"id": remote_id}) @classmethod @@ -110,7 +126,7 @@ class ActivitypubMixin: 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), @@ -118,7 +134,7 @@ class ActivitypubMixin: ) 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" # is this activity owned by a user (statuses, lists, shelves), or is it @@ -132,13 +148,17 @@ class ActivitypubMixin: 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 []] + recipients = [u.inbox for u in mentions or [] if not u.local] # unless it's a dm, all the followers should receive the activity if privacy != "direct": # we will send this out to a subset of all remote users - queryset = user_model.objects.filter( - local=False, + queryset = ( + user_model.viewer_aware_objects(user) + .filter( + local=False, + ) + .distinct() ) # filter users first by whether they're using the desired software # this lets us send book updates only to other bw servers @@ -159,32 +179,34 @@ class ActivitypubMixin: "inbox", flat=True ) recipients += list(shared_inboxes) + list(inboxes) - return recipients + return list(set(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 """ + """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 created/updated/deleted objects as appropriate""" broadcast = kwargs.get("broadcast", True) - # this bonus kwarg woul cause an error in the base save method + # this bonus kwarg would cause an error in the base save method if "broadcast" in kwargs: del kwargs["broadcast"] created = created or not bool(self.id) # first off, we want to save normally no matter what super().save(*args, **kwargs) - if not broadcast: + if not broadcast or ( + hasattr(self, "status_type") and self.status_type == "Announce" + ): return # this will work for objects owned by a user (lists, shelves) @@ -232,7 +254,7 @@ class ObjectMixin(ActivitypubMixin): 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 @@ -258,7 +280,7 @@ class ObjectMixin(ActivitypubMixin): ).serialize() def to_delete_activity(self, user): - """ notice of deletion """ + """notice of deletion""" return activitypub.Delete( id=self.remote_id + "/activity", actor=user.remote_id, @@ -268,7 +290,7 @@ class ObjectMixin(ActivitypubMixin): ).serialize() def to_update_activity(self, user): - """ wrapper for Updates to an activity """ + """wrapper for Updates to an activity""" activity_id = "%s#update/%s" % (self.remote_id, uuid4()) return activitypub.Update( id=activity_id, @@ -284,13 +306,13 @@ class OrderedCollectionPageMixin(ObjectMixin): @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 """ + """an ordered collection of whatevers""" if not queryset.ordered: raise RuntimeError("queryset must be ordered") @@ -319,11 +341,11 @@ class OrderedCollectionPageMixin(ObjectMixin): 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 """ + """usually an ordered collection model aggregates a different model""" raise NotImplementedError("Model must define collection_queryset") activity_serializer = activitypub.OrderedCollection @@ -332,81 +354,98 @@ 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() 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 + activity_serializer = activitypub.CollectionItem + + def broadcast(self, activity, sender, software="bookwyrm"): + """only send book collection updates to other bookwyrm instances""" + super().broadcast(activity, sender, software=software) + + @property + def privacy(self): + """inherit the privacy of the list, or direct if pending""" + collection_field = getattr(self, self.collection_field) + if self.approved: + return collection_field.privacy + return "direct" + + @property + def recipients(self): + """the owner of the list is a direct recipient""" + collection_field = getattr(self, self.collection_field) + if collection_field.user.local: + # don't broadcast to yourself + return [] + return [collection_field.user] def save(self, *args, broadcast=True, **kwargs): - """ broadcast updated """ - created = not bool(self.id) + """broadcast updated""" # first off, we want to save normally no matter what super().save(*args, **kwargs) - # these shouldn't be edited, only created and deleted - if not broadcast or not created or not self.user.local: + # list items can be updateda, normally you would only broadcast on created + if not broadcast or not self.user.local: return # adding an obj to the collection - activity = self.to_add_activity() + activity = self.to_add_activity(self.user) self.broadcast(activity, self.user) - def delete(self, *args, **kwargs): - """ broadcast a remove activity """ - activity = self.to_remove_activity() + def delete(self, *args, broadcast=True, **kwargs): + """broadcast a remove activity""" + activity = self.to_remove_activity(self.user) super().delete(*args, **kwargs) - if self.user.local: + if self.user.local and broadcast: self.broadcast(activity, self.user) - def to_add_activity(self): - """ AP for shelving a book""" - object_field = getattr(self, self.object_field) + def to_add_activity(self, user): + """AP for shelving a book""" collection_field = getattr(self, self.collection_field) return activitypub.Add( - id=self.remote_id, - actor=self.user.remote_id, - object=object_field, + id="{:s}#add".format(collection_field.remote_id), + actor=user.remote_id, + object=self.to_activity_dataclass(), target=collection_field.remote_id, ).serialize() - def to_remove_activity(self): - """ AP for un-shelving a book""" - object_field = getattr(self, self.object_field) + def to_remove_activity(self, user): + """AP for un-shelving a book""" collection_field = getattr(self, self.collection_field) return activitypub.Remove( - id=self.remote_id, - actor=self.user.remote_id, - object=object_field, + id="{:s}#remove".format(collection_field.remote_id), + actor=user.remote_id, + object=self.to_activity_dataclass(), 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 if broadcast and user.local: self.broadcast(self.to_activity(), user) def delete(self, *args, broadcast=True, **kwargs): - """ nevermind, undo that activity """ + """nevermind, undo that activity""" user = self.user if hasattr(self, "user") else self.user_subject if broadcast and user.local: self.broadcast(self.to_undo_activity(), user) super().delete(*args, **kwargs) def to_undo_activity(self): - """ undo an action """ + """undo an action""" user = self.user if hasattr(self, "user") else self.user_subject return activitypub.Undo( id="%s#undo" % self.remote_id, @@ -416,7 +455,7 @@ class ActivityMixin(ActivitypubMixin): 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) @@ -430,7 +469,7 @@ def generate_activity(obj): ) in obj.serialize_reverse_fields: related_field = getattr(obj, model_field_name) activity[activity_field_name] = unfurl_related_field( - related_field, sort_field + related_field, sort_field=sort_field ) if not activity.get("id"): @@ -439,8 +478,8 @@ def generate_activity(obj): def unfurl_related_field(related_field, sort_field=None): - """ load reverse lookups (like public key owner or Status attachment """ - if hasattr(related_field, "all"): + """load reverse lookups (like public key owner or Status attachment""" + if sort_field and hasattr(related_field, "all"): return [ unfurl_related_field(i) for i in related_field.order_by(sort_field).all() ] @@ -455,7 +494,7 @@ def unfurl_related_field(related_field, sort_field=None): @app.task def broadcast_task(sender_id, activity, recipients): - """ the celery task for broadcast """ + """the celery task for broadcast""" user_model = apps.get_model("bookwyrm.User", require_ready=True) sender = user_model.objects.get(id=sender_id) for recipient in recipients: @@ -466,7 +505,7 @@ def broadcast_task(sender_id, activity, recipients): 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: @@ -495,10 +534,10 @@ def sign_and_send(sender, data, destination): def to_ordered_collection_page( queryset, remote_id, id_only=False, page=1, pure=False, **kwargs ): - """ serialize and pagiante a queryset """ + """serialize and pagiante a queryset""" paginated = Paginator(queryset, PAGE_LENGTH) - activity_page = paginated.page(page) + activity_page = paginated.get_page(page) if id_only: items = [s.remote_id for s in activity_page.object_list] else: diff --git a/bookwyrm/models/attachment.py b/bookwyrm/models/attachment.py index 8d2238a1..c8b2e51c 100644 --- a/bookwyrm/models/attachment.py +++ b/bookwyrm/models/attachment.py @@ -8,7 +8,7 @@ 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 @@ -16,13 +16,13 @@ class Attachment(ActivitypubMixin, BookWyrmModel): 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/", @@ -33,4 +33,4 @@ class Image(Attachment): ) caption = fields.TextField(null=True, blank=True, activitypub_field="name") - activity_serializer = activitypub.Image + activity_serializer = activitypub.Document diff --git a/bookwyrm/models/author.py b/bookwyrm/models/author.py index f7740b1d..c4e26c5a 100644 --- a/bookwyrm/models/author.py +++ b/bookwyrm/models/author.py @@ -9,7 +9,7 @@ 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 @@ -33,7 +33,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 """ + """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 cb2fc851..e85ff733 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -7,14 +7,14 @@ 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") def get_remote_id(self): - """ generate a url that resolves to the local object """ + """generate a url that resolves to the local object""" base_path = "https://%s" % DOMAIN if hasattr(self, "user"): base_path = "%s%s" % (base_path, self.user.local_path) @@ -22,20 +22,50 @@ class BookWyrmModel(models.Model): 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 """ + """how to link to this object in the local app""" return self.get_remote_id().replace("https://%s" % DOMAIN, "") + def visible_to_user(self, viewer): + """is a user authorized to view an object?""" + # make sure this is an object with privacy owned by a user + if not hasattr(self, "user") or not hasattr(self, "privacy"): + return None + + # viewer can't see it if the object's owner blocked them + if viewer in self.user.blocks.all(): + return False + + # you can see your own posts and any public or unlisted posts + if viewer == self.user or self.privacy in ["public", "unlisted"]: + return True + + # you can see the followers only posts of people you follow + if ( + self.privacy == "followers" + and self.user.followers.filter(id=viewer.id).first() + ): + return True + + # you can see dms you are tagged in + if hasattr(self, "mention_users"): + if ( + self.privacy == "direct" + and self.mention_users.filter(id=viewer.id).first() + ): + return True + return False + @receiver(models.signals.post_save) # pylint: disable=unused-argument def set_remote_id(sender, instance, created, *args, **kwargs): - """ set the remote_id after save (when the id is available) """ + """set the remote_id after save (when the id is available)""" if not created or not hasattr(instance, "get_remote_id"): return if not instance.remote_id: diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 94bbe330..10ebb317 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -13,7 +13,7 @@ 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( @@ -32,15 +32,19 @@ class BookDataModel(ObjectMixin, BookWyrmModel): 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 = fields.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: @@ -49,24 +53,24 @@ class BookDataModel(ObjectMixin, BookWyrmModel): return super().save(*args, **kwargs) def broadcast(self, activity, sender, software="bookwyrm"): - """ only send book data updates to other bookwyrm instances """ + """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 """ + """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) + title = fields.TextField(max_length=255) sort_title = fields.CharField(max_length=255, blank=True, null=True) - subtitle = fields.CharField(max_length=255, blank=True, null=True) + subtitle = fields.TextField(max_length=255, blank=True, null=True) description = fields.HtmlField(blank=True, null=True) languages = fields.ArrayField( models.CharField(max_length=255), blank=True, default=list ) - series = fields.CharField(max_length=255, blank=True, null=True) + series = fields.TextField(max_length=255, blank=True, null=True) series_number = fields.CharField(max_length=255, blank=True, null=True) subjects = fields.ArrayField( models.CharField(max_length=255), blank=True, null=True, default=list @@ -85,17 +89,17 @@ class Book(BookDataModel): @property def author_text(self): - """ format a list of authors """ + """format a list of authors""" return ", ".join(a.name for a in self.authors.all()) @property def latest_readthrough(self): - """ most recent readthrough activity """ + """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" @@ -108,20 +112,20 @@ class Book(BookDataModel): @property def alt_text(self): - """ image alt test """ + """image alt test""" text = "%s" % self.title if 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") return super().save(*args, **kwargs) def get_remote_id(self): - """ editions and works both use "book" instead of model_name """ + """editions and works both use "book" instead of model_name""" return "https://%s/book/%d" % (DOMAIN, self.id) def __repr__(self): @@ -133,7 +137,7 @@ 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( @@ -145,19 +149,19 @@ class Work(OrderedCollectionPageMixin, Book): ) 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 """ + """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 """ + """sets a new default edition based on computed rank""" self.default_edition = None # editions are re-ranked implicitly self.save() @@ -165,11 +169,11 @@ class Work(OrderedCollectionPageMixin, Book): 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, - **kwargs + **kwargs, ) activity_serializer = activitypub.Work @@ -178,7 +182,7 @@ class Work(OrderedCollectionPageMixin, Book): 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( @@ -217,7 +221,7 @@ class Edition(Book): name_field = "title" def get_rank(self, ignore_default=False): - """ calculate how complete the data is on this edition """ + """calculate how complete the data is on this edition""" if ( not ignore_default and self.parent_work @@ -237,7 +241,7 @@ 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: self.isbn_10 = isbn_13_to_10(self.isbn_13) @@ -251,7 +255,7 @@ class Edition(Book): def isbn_10_to_13(isbn_10): - """ convert an isbn 10 into an isbn 13 """ + """convert an isbn 10 into an isbn 13""" isbn_10 = re.sub(r"[^0-9X]", "", isbn_10) # drop the last character of the isbn 10 number (the original checkdigit) converted = isbn_10[:9] @@ -273,7 +277,7 @@ def isbn_10_to_13(isbn_10): def isbn_13_to_10(isbn_13): - """ convert isbn 13 to 10, if possible """ + """convert isbn 13 to 10, if possible""" if isbn_13[:3] != "978": return None diff --git a/bookwyrm/models/connector.py b/bookwyrm/models/connector.py index 9f9af8ae..625cdbed 100644 --- a/bookwyrm/models/connector.py +++ b/bookwyrm/models/connector.py @@ -9,7 +9,7 @@ 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) diff --git a/bookwyrm/models/favorite.py b/bookwyrm/models/favorite.py index 7b72d175..c4518119 100644 --- a/bookwyrm/models/favorite.py +++ b/bookwyrm/models/favorite.py @@ -11,7 +11,7 @@ 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" @@ -24,11 +24,11 @@ class Favorite(ActivityMixin, BookWyrmModel): @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) @@ -45,7 +45,7 @@ class Favorite(ActivityMixin, BookWyrmModel): ) 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( @@ -62,6 +62,6 @@ class Favorite(ActivityMixin, BookWyrmModel): super().delete(*args, **kwargs) class Meta: - """ can't fav things twice """ + """can't fav things twice""" unique_together = ("user", "status") diff --git a/bookwyrm/models/federated_server.py b/bookwyrm/models/federated_server.py index 8f7d903e..7d446ca0 100644 --- a/bookwyrm/models/federated_server.py +++ b/bookwyrm/models/federated_server.py @@ -1,17 +1,51 @@ """ connections to external ActivityPub servers """ +from urllib.parse import urlparse from django.db import models from .base_model import BookWyrmModel +FederationStatus = models.TextChoices( + "Status", + [ + "federated", + "blocked", + ], +) + class FederatedServer(BookWyrmModel): - """ store which servers 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", choices=FederationStatus.choices + ) # is it mastodon, bookwyrm, etc - application_type = models.CharField(max_length=255, null=True) - application_version = models.CharField(max_length=255, null=True) + application_type = models.CharField(max_length=255, null=True, blank=True) + application_version = models.CharField(max_length=255, null=True, blank=True) + notes = models.TextField(null=True, blank=True) + def block(self): + """block a server""" + self.status = "blocked" + self.save() -# TODO: blocked servers + # deactivate all associated users + self.user_set.filter(is_active=True).update( + is_active=False, deactivation_reason="domain_block" + ) + + def unblock(self): + """unblock a server""" + self.status = "federated" + self.save() + + self.user_set.filter(deactivation_reason="domain_block").update( + is_active=True, deactivation_reason=None + ) + + @classmethod + def is_blocked(cls, url): + """look up if a domain is blocked""" + url = urlparse(url) + domain = url.netloc + return cls.objects.filter(server_name=domain, status="blocked").exists() diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index e034d59e..123b3efa 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -18,7 +18,7 @@ from bookwyrm.settings import DOMAIN def validate_remote_id(value): - """ make sure the remote_id looks like a url """ + """make sure the remote_id looks like a url""" if not value or not re.match(r"^http.?:\/\/[^\s]+$", value): raise ValidationError( _("%(value)s is not a valid remote_id"), @@ -27,7 +27,7 @@ def validate_remote_id(value): def validate_localname(value): - """ make sure localnames look okay """ + """make sure localnames look okay""" if not re.match(r"^[A-Za-z\-_\.0-9]+$", value): raise ValidationError( _("%(value)s is not a valid username"), @@ -36,7 +36,7 @@ def validate_localname(value): def validate_username(value): - """ make sure usernames look okay """ + """make sure usernames look okay""" if not re.match(r"^[A-Za-z\-_\.0-9]+@[A-Za-z\-_\.0-9]+\.[a-z]{2,}$", value): raise ValidationError( _("%(value)s is not a valid username"), @@ -45,7 +45,7 @@ def validate_username(value): class ActivitypubFieldMixin: - """ make a database field serializable """ + """make a database field serializable""" def __init__( self, @@ -64,7 +64,7 @@ class ActivitypubFieldMixin: 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: @@ -78,7 +78,7 @@ class ActivitypubFieldMixin: 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: @@ -94,19 +94,19 @@ class ActivitypubFieldMixin: activity[key] = formatted def field_to_activity(self, value): - """ formatter to convert a model value into activitypub """ + """formatter to convert a model value into activitypub""" if hasattr(self, "activitypub_wrapper"): return {self.activitypub_wrapper: value} return value def field_from_activity(self, value): - """ formatter to convert activitypub into a model value """ + """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] @@ -115,7 +115,7 @@ class ActivitypubFieldMixin: class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin): - """ default (de)serialization for foreign key and one to one """ + """default (de)serialization for foreign key and one to one""" def __init__(self, *args, load_remote=True, **kwargs): self.load_remote = load_remote @@ -146,7 +146,7 @@ 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] @@ -156,7 +156,7 @@ class RemoteIdField(ActivitypubFieldMixin, models.CharField): class UsernameField(ActivitypubFieldMixin, models.CharField): - """ activitypub-aware username field """ + """activitypub-aware username field""" def __init__(self, activitypub_field="preferredUsername", **kwargs): self.activitypub_field = activitypub_field @@ -172,7 +172,7 @@ class UsernameField(ActivitypubFieldMixin, models.CharField): ) 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"] @@ -191,7 +191,7 @@ PrivacyLevels = models.TextChoices( class PrivacyField(ActivitypubFieldMixin, models.CharField): - """ this maps to two differente activitypub fields """ + """this maps to two differente activitypub fields""" public = "https://www.w3.org/ns/activitystreams#Public" @@ -236,7 +236,7 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField): class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey): - """ activitypub-aware foreign key field """ + """activitypub-aware foreign key field""" def field_to_activity(self, value): if not value: @@ -245,7 +245,7 @@ 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: @@ -254,14 +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: @@ -275,9 +275,12 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): return [i.remote_id for i in value.all()] def field_from_activity(self, value): - items = [] if value is None or value is MISSING: - return [] + return None + if not isinstance(value, list): + # If this is a link, we currently aren't doing anything with it + return None + items = [] for remote_id in value: try: validate_remote_id(remote_id) @@ -290,7 +293,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): class TagField(ManyToManyField): - """ special case of many to many that uses Tags """ + """special case of many to many that uses Tags""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -330,17 +333,17 @@ class TagField(ManyToManyField): def image_serializer(value, alt): - """ helper for serializing images """ + """helper for serializing images""" if value and hasattr(value, "url"): url = value.url else: return None url = "https://%s%s" % (DOMAIN, url) - return activitypub.Image(url=url, name=alt) + return activitypub.Document(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 @@ -348,7 +351,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField): # 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: @@ -394,7 +397,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField): class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): - """ activitypub-aware datetime field """ + """activitypub-aware datetime field""" def field_to_activity(self, value): if not value: @@ -413,7 +416,7 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): 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: @@ -424,30 +427,30 @@ class HtmlField(ActivitypubFieldMixin, models.TextField): 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 """ + """activitypub-aware boolean field""" def field_to_activity(self, value): if not value: diff --git a/bookwyrm/models/import_job.py b/bookwyrm/models/import_job.py index 026cf7cd..1b1152ab 100644 --- a/bookwyrm/models/import_job.py +++ b/bookwyrm/models/import_job.py @@ -20,7 +20,7 @@ GOODREADS_SHELVES = { def unquote_string(text): - """ resolve csv quote weirdness """ + """resolve csv quote weirdness""" match = re.match(r'="([^"]*)"', text) if match: return match.group(1) @@ -28,7 +28,7 @@ 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) # Open library doesn't like including author initials in search term. @@ -38,7 +38,7 @@ def construct_search_term(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) @@ -51,7 +51,7 @@ class ImportJob(models.Model): 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( @@ -65,7 +65,7 @@ class ImportJob(models.Model): class ImportItem(models.Model): - """ a single line of a csv being imported """ + """a single line of a csv being imported""" job = models.ForeignKey(ImportJob, on_delete=models.CASCADE, related_name="items") index = models.IntegerField() @@ -74,11 +74,11 @@ class ImportItem(models.Model): fail_reason = models.TextField(null=True) def resolve(self): - """ try various ways to lookup a book """ + """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 ) @@ -88,7 +88,7 @@ class ImportItem(models.Model): return None def get_book_from_title_author(self): - """ search by title and 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 @@ -100,60 +100,60 @@ class ImportItem(models.Model): @property def title(self): - """ get the book title """ + """get the book title""" return self.data["Title"] @property def author(self): - """ get the book title """ + """get the book title""" return self.data["Author"] @property def isbn(self): - """ pulls out the isbn13 field from the csv line data """ + """pulls out the isbn13 field from the csv line data""" return unquote_string(self.data["ISBN13"]) @property def shelf(self): - """ the goodreads shelf field """ + """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 """ + """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 """ + """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 """ + """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 """ + """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 """ + """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) diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 880c4122..2a5c3382 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -21,7 +21,7 @@ CurationType = models.TextChoices( class List(OrderedCollectionMixin, BookWyrmModel): - """ a list of books """ + """a list of books""" name = fields.CharField(max_length=100) user = fields.ForeignKey( @@ -41,43 +41,40 @@ class List(OrderedCollectionMixin, BookWyrmModel): activity_serializer = activitypub.BookList def get_remote_id(self): - """ don't want the user to be in there in this case """ + """don't want the user to be in there in this case""" return "https://%s/list/%d" % (DOMAIN, self.id) @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).order_by("listitem") class Meta: - """ default sorting """ + """default sorting""" ordering = ("-updated_date",) class ListItem(CollectionItemMixin, BookWyrmModel): - """ ok """ + """ok""" book = fields.ForeignKey( - "Edition", on_delete=models.PROTECT, activitypub_field="object" - ) - book_list = fields.ForeignKey( - "List", on_delete=models.CASCADE, activitypub_field="target" + "Edition", on_delete=models.PROTECT, activitypub_field="book" ) + book_list = models.ForeignKey("List", on_delete=models.CASCADE) user = fields.ForeignKey( "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) + order = fields.IntegerField() endorsement = models.ManyToManyField("User", related_name="endorsers") - activity_serializer = activitypub.Add - object_field = "book" + activity_serializer = activitypub.ListItem 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) # tick the updated date on the parent list @@ -96,7 +93,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel): ) class Meta: - """ an opinionated constraint! you can't put a book on a list twice """ - - unique_together = ("book", "book_list") + # A book may only be placed into a list once, and each order in the list may be used only + # once + unique_together = (("book", "book_list"), ("order", "book_list")) ordering = ("-created_date",) diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py index 233d635b..ff0b4e5a 100644 --- a/bookwyrm/models/notification.py +++ b/bookwyrm/models/notification.py @@ -10,7 +10,7 @@ NotificationType = models.TextChoices( class Notification(BookWyrmModel): - """ you've been tagged, liked, followed, etc """ + """you've been tagged, liked, followed, etc""" user = models.ForeignKey("User", on_delete=models.CASCADE) related_book = models.ForeignKey("Edition", on_delete=models.CASCADE, null=True) @@ -29,7 +29,7 @@ class Notification(BookWyrmModel): ) 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, @@ -45,7 +45,7 @@ class Notification(BookWyrmModel): 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( diff --git a/bookwyrm/models/readthrough.py b/bookwyrm/models/readthrough.py index 1a5fcb0d..664daa13 100644 --- a/bookwyrm/models/readthrough.py +++ b/bookwyrm/models/readthrough.py @@ -7,14 +7,14 @@ from .base_model import BookWyrmModel class ProgressMode(models.TextChoices): - """ types of prgress available """ + """types of prgress available""" PAGE = "PG", "page" PERCENT = "PCT", "percent" class ReadThrough(BookWyrmModel): - """ Store a read through a book in the database. """ + """Store a read through a book in the database.""" user = models.ForeignKey("User", on_delete=models.PROTECT) book = models.ForeignKey("Edition", on_delete=models.PROTECT) @@ -28,13 +28,13 @@ class ReadThrough(BookWyrmModel): 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) def create_update(self): - """ add update to the readthrough """ + """add update to the readthrough""" if self.progress: return self.progressupdate_set.create( user=self.user, progress=self.progress, mode=self.progress_mode @@ -43,7 +43,7 @@ class ReadThrough(BookWyrmModel): class ProgressUpdate(BookWyrmModel): - """ Store progress through a book in the database. """ + """Store progress through a book in the database.""" user = models.ForeignKey("User", on_delete=models.PROTECT) readthrough = models.ForeignKey("ReadThrough", on_delete=models.CASCADE) @@ -53,7 +53,7 @@ class ProgressUpdate(BookWyrmModel): ) 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 998d7bed..12f4c51a 100644 --- a/bookwyrm/models/relationship.py +++ b/bookwyrm/models/relationship.py @@ -11,7 +11,7 @@ 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", @@ -28,16 +28,16 @@ class UserRelationship(BookWyrmModel): @property def privacy(self): - """ all relationships are handled directly with the participants """ + """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 = [ @@ -50,24 +50,23 @@ class UserRelationship(BookWyrmModel): ), ] - 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): + """use shelf identifier in remote_id""" base_path = self.user_subject.remote_id - return "%s#%s/%d" % (base_path, status, self.id) + return "%s#follows/%d" % (base_path, self.id) class UserFollows(ActivityMixin, UserRelationship): - """ Following a user """ + """Following a user""" status = "follows" def to_activity(self): # pylint: disable=arguments-differ - """ 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( @@ -86,7 +85,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, @@ -95,19 +94,22 @@ class UserFollows(ActivityMixin, UserRelationship): class UserFollowRequest(ActivitypubMixin, UserRelationship): - """ following a user requires manual or automatic confirmation """ + """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 """ - # don't create a request if a follow already exists + """make sure the follow or block relationship doesn't already exist""" + # if there's a request for a follow that already exists, accept it + # without changing the local database state if UserFollows.objects.filter( user_subject=self.user_subject, user_object=self.user_object, ).exists(): - raise IntegrityError() + self.accept(broadcast_only=True) + return + # blocking in either direction is a no-go if UserBlocks.objects.filter( Q( @@ -138,25 +140,34 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship): notification_type=notification_type, ) - def accept(self): - """ turn this request into the real deal""" + def get_accept_reject_id(self, status): + """get id for sending an accept or reject of a local user""" + + base_path = self.user_object.remote_id + return "%s#%s/%d" % (base_path, status, self.id or 0) + + def accept(self, broadcast_only=False): + """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_accept_reject_id(status="accepts"), actor=self.user_object.remote_id, object=self.to_activity(), ).serialize() self.broadcast(activity, user) + if broadcast_only: + return + 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_accept_reject_id(status="rejects"), actor=self.user_object.remote_id, object=self.to_activity(), ).serialize() @@ -166,13 +177,13 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship): class UserBlocks(ActivityMixin, UserRelationship): - """ prevent another user from following you and seeing your posts """ + """prevent another user from following you and seeing your posts""" status = "blocks" 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( diff --git a/bookwyrm/models/report.py b/bookwyrm/models/report.py index f9e8905b..7ff4c909 100644 --- a/bookwyrm/models/report.py +++ b/bookwyrm/models/report.py @@ -6,7 +6,7 @@ from .base_model import BookWyrmModel class Report(BookWyrmModel): - """ reported status or user """ + """reported status or user""" reporter = models.ForeignKey( "User", related_name="reporter", on_delete=models.PROTECT @@ -17,7 +17,7 @@ class Report(BookWyrmModel): resolved = models.BooleanField(default=False) def save(self, *args, **kwargs): - """ notify admins when a report is created """ + """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 @@ -34,7 +34,7 @@ class Report(BookWyrmModel): ) class Meta: - """ don't let users report themselves """ + """don't let users report themselves""" constraints = [ models.CheckConstraint(check=~Q(reporter=F("user")), name="self_report") @@ -43,13 +43,13 @@ class Report(BookWyrmModel): class ReportComment(BookWyrmModel): - """ updates on a report """ + """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 """ + """sort comments""" ordering = ("-created_date",) diff --git a/bookwyrm/models/shelf.py b/bookwyrm/models/shelf.py index 3209da5d..4110ae8d 100644 --- a/bookwyrm/models/shelf.py +++ b/bookwyrm/models/shelf.py @@ -9,7 +9,7 @@ from . import fields class Shelf(OrderedCollectionMixin, BookWyrmModel): - """ a list of books owned by a user """ + """a list of books owned by a user""" TO_READ = "to-read" READING = "reading" @@ -34,49 +34,46 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel): activity_serializer = activitypub.Shelf def save(self, *args, **kwargs): - """ set the identifier """ + """set the identifier""" super().save(*args, **kwargs) if not self.identifier: self.identifier = self.get_identifier() super().save(*args, **kwargs, broadcast=False) def get_identifier(self): - """ custom-shelf-123 for the url """ + """custom-shelf-123 for the url""" slug = re.sub(r"[^\w]", "", self.name).lower() return "{:s}-{:d}".format(slug, self.id) @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.order_by("shelfbook") def get_remote_id(self): - """ shelf identifier instead of id """ + """shelf identifier instead of id""" base_path = self.user.remote_id identifier = self.identifier or self.get_identifier() return "%s/books/%s" % (base_path, identifier) class Meta: - """ user/shelf unqiueness """ + """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" - ) - shelf = fields.ForeignKey( - "Shelf", on_delete=models.PROTECT, activitypub_field="target" + "Edition", on_delete=models.PROTECT, activitypub_field="book" ) + shelf = models.ForeignKey("Shelf", on_delete=models.PROTECT) user = fields.ForeignKey( "User", on_delete=models.PROTECT, activitypub_field="actor" ) - activity_serializer = activitypub.Add - object_field = "book" + activity_serializer = activitypub.ShelfItem collection_field = "shelf" def save(self, *args, **kwargs): diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index 1eb31869..193cffb7 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -12,7 +12,7 @@ from .user import User class SiteSettings(models.Model): - """ customized settings for this instance """ + """customized settings for this instance""" name = models.CharField(default="BookWyrm", max_length=100) instance_tagline = models.CharField( @@ -35,7 +35,7 @@ class SiteSettings(models.Model): @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: @@ -45,12 +45,12 @@ class SiteSettings(models.Model): def new_access_code(): - """ the identifier for a user invite """ + """the identifier for a user invite""" return base64.b32encode(Random.get_random_bytes(5)).decode("ascii") 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) @@ -61,19 +61,19 @@ class SiteInvite(models.Model): invitees = models.ManyToManyField(User, related_name="invitees") def valid(self): - """ make sure it hasn't expired or been used """ + """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 """ + """formats the invite link""" return "https://{}/invite/{}".format(DOMAIN, self.code) class InviteRequest(BookWyrmModel): - """ prospective users can request an invite """ + """prospective users can request an invite""" email = models.EmailField(max_length=255, unique=True) invite = models.ForeignKey( @@ -83,30 +83,30 @@ class InviteRequest(BookWyrmModel): ignored = models.BooleanField(default=False) def save(self, *args, **kwargs): - """ don't create a request for a registered email """ + """don't create a request for a registered email""" if not self.id and User.objects.filter(email=self.email).exists(): raise IntegrityError() super().save(*args, **kwargs) 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 """ + """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 360288e9..bd21ec56 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -19,7 +19,7 @@ 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" @@ -59,12 +59,12 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): deserialize_reverse_fields = [("attachments", "attachment")] class Meta: - """ default sorting """ + """default sorting""" ordering = ("-published_date",) 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) @@ -98,7 +98,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): ) def delete(self, *args, **kwargs): # pylint: disable=unused-argument - """ "delete" a status """ + """ "delete" a status""" if hasattr(self, "boosted_status"): # okay but if it's a boost really delete it super().delete(*args, **kwargs) @@ -109,7 +109,7 @@ 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") @@ -121,7 +121,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): @classmethod def ignore_activity(cls, activity): # pylint: disable=too-many-return-statements - """ keep notes if they are replies to existing statuses """ + """keep notes if they are replies to existing statuses""" if activity.type == "Announce": try: boosted = activitypub.resolve_remote_id( @@ -163,16 +163,16 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): @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 """ + """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, @@ -181,7 +181,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): ).serialize() def to_activity_dataclass(self, pure=False): # pylint: disable=arguments-differ - """ return tombstone if the status is deleted """ + """return tombstone if the status is deleted""" if self.deleted: return activitypub.Tombstone( id=self.remote_id, @@ -210,16 +210,16 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): return activity def to_activity(self, pure=False): # pylint: disable=arguments-differ - """ json serialized activitypub class """ + """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) @@ -232,7 +232,7 @@ class GeneratedNote(Status): 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" @@ -253,7 +253,7 @@ class Comment(Status): @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 '%s

(comment on "%s")

' % ( self.content, self.book.remote_id, @@ -265,7 +265,7 @@ class Comment(Status): 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( @@ -274,7 +274,7 @@ class Quotation(Status): @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""" quote = re.sub(r"^

", '

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

$", '"

', quote) return '%s

-- "%s"

%s' % ( @@ -289,7 +289,7 @@ class Quotation(Status): class Review(Status): - """ a book review """ + """a book review""" name = fields.CharField(max_length=255, null=True) book = fields.ForeignKey( @@ -306,7 +306,7 @@ class Review(Status): @property def pure_name(self): - """ clarify review names for mastodon serialization """ + """clarify review names for mastodon serialization""" template = get_template("snippets/generated_status/review_pure_name.html") return template.render( {"book": self.book, "rating": self.rating, "name": self.name} @@ -314,7 +314,7 @@ class Review(Status): @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 @@ -322,7 +322,7 @@ class Review(Status): class ReviewRating(Review): - """ a subtype of review that only contains a rating """ + """a subtype of review that only contains a rating""" def save(self, *args, **kwargs): if not self.rating: @@ -339,7 +339,7 @@ class ReviewRating(Review): class Boost(ActivityMixin, Status): - """ boost'ing a post """ + """boost'ing a post""" boosted_status = fields.ForeignKey( "Status", @@ -350,7 +350,17 @@ class Boost(ActivityMixin, Status): activity_serializer = activitypub.Announce def save(self, *args, **kwargs): - """ save and notify """ + """save and notify""" + # This constraint can't work as it would cross tables. + # class Meta: + # unique_together = ('user', 'boosted_status') + if ( + Boost.objects.filter(boosted_status=self.boosted_status, user=self.user) + .exclude(id=self.id) + .exists() + ): + return + super().save(*args, **kwargs) if not self.boosted_status.user.local or self.boosted_status.user == self.user: return @@ -364,7 +374,7 @@ class Boost(ActivityMixin, Status): ) def delete(self, *args, **kwargs): - """ delete and un-notify """ + """delete and un-notify""" notification_model = apps.get_model("bookwyrm.Notification", require_ready=True) notification_model.objects.filter( user=self.boosted_status.user, @@ -375,7 +385,7 @@ class Boost(ActivityMixin, Status): 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", "published_date", "privacy"] diff --git a/bookwyrm/models/tag.py b/bookwyrm/models/tag.py deleted file mode 100644 index 2c45b8f9..00000000 --- a/bookwyrm/models/tag.py +++ /dev/null @@ -1,63 +0,0 @@ -""" models for storing different kinds of Activities """ -import urllib.parse - -from django.apps import apps -from django.db import models - -from bookwyrm import activitypub -from bookwyrm.settings import DOMAIN -from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin -from .base_model import BookWyrmModel -from . import fields - - -class Tag(OrderedCollectionMixin, BookWyrmModel): - """ 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() - ) - - 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) - - def save(self, *args, **kwargs): - """ create a url-safe lookup key for the tag """ - if not self.id: - # add identifiers to new tags - self.identifier = urllib.parse.quote_plus(self.name) - super().save(*args, **kwargs) - - -class UserTag(CollectionItemMixin, BookWyrmModel): - """ an instance of a tag on a book by a user """ - - user = fields.ForeignKey( - "User", on_delete=models.PROTECT, activitypub_field="actor" - ) - book = fields.ForeignKey( - "Edition", on_delete=models.PROTECT, activitypub_field="object" - ) - tag = fields.ForeignKey("Tag", on_delete=models.PROTECT, activitypub_field="target") - - activity_serializer = activitypub.Add - object_field = "book" - collection_field = "tag" - - class Meta: - """ unqiueness constraint """ - - unique_together = ("user", "book", "tag") diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 33dedc9e..3efbd6ac 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -4,6 +4,7 @@ from urllib.parse import urlparse from django.apps import apps from django.contrib.auth.models import AbstractUser, Group +from django.contrib.postgres.fields import CICharField from django.core.validators import MinValueValidator from django.db import models from django.utils import timezone @@ -23,8 +24,18 @@ from .federated_server import FederatedServer from . import fields, Review +DeactivationReason = models.TextChoices( + "DeactivationReason", + [ + "self_deletion", + "moderator_deletion", + "domain_block", + ], +) + + 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) @@ -54,7 +65,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): summary = fields.HtmlField(null=True, blank=True) local = models.BooleanField(default=False) bookwyrm_user = fields.BooleanField(default=True) - localname = models.CharField( + localname = CICharField( max_length=255, null=True, unique=True, @@ -110,33 +121,47 @@ class User(OrderedCollectionPageMixin, AbstractUser): default=str(pytz.utc), max_length=255, ) + deactivation_reason = models.CharField( + max_length=255, choices=DeactivationReason.choices, null=True, blank=True + ) name_field = "username" + property_fields = [("following_link", "following")] + + @property + def following_link(self): + """just how to find out the following info""" + return "{:s}/following".format(self.remote_id) @property def alt_text(self): - """ alt text with 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 """ + """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 + @property + def deleted(self): + """for consistent naming""" + return not self.is_active + activity_serializer = activitypub.Person @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: + if viewer and viewer.is_authenticated: 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 @@ -163,7 +188,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): ).serialize() def to_following_activity(self, **kwargs): - """ activitypub following list """ + """activitypub following list""" remote_id = "%s/following" % self.remote_id return self.to_ordered_collection( self.following.order_by("-updated_date").all(), @@ -173,7 +198,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): ) def to_followers_activity(self, **kwargs): - """ activitypub followers list """ + """activitypub followers list""" remote_id = "%s/followers" % self.remote_id return self.to_ordered_collection( self.followers.order_by("-updated_date").all(), @@ -185,6 +210,9 @@ class User(OrderedCollectionPageMixin, AbstractUser): def to_activity(self, **kwargs): """override default AP serializer to add context object idk if this is the best way to go about this""" + if not self.is_active: + return self.remote_id + activity_object = super().to_activity(**kwargs) activity_object["@context"] = [ "https://www.w3.org/ns/activitystreams", @@ -199,7 +227,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): 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) @@ -263,14 +291,20 @@ class User(OrderedCollectionPageMixin, AbstractUser): editable=False, ).save(broadcast=False) + def delete(self, *args, **kwargs): + """deactivate rather than delete a user""" + self.is_active = False + # skip the logic in this class's save() + super().save(*args, **kwargs) + @property def local_path(self): - """ this model doesn't inherit bookwyrm model, so here we are """ + """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( @@ -285,7 +319,7 @@ class KeyPair(ActivitypubMixin, BookWyrmModel): 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"] @@ -303,7 +337,7 @@ class KeyPair(ActivitypubMixin, BookWyrmModel): class AnnualGoal(BookWyrmModel): - """ set a goal for how many books you read in a year """ + """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)]) @@ -313,17 +347,17 @@ class AnnualGoal(BookWyrmModel): ) class Meta: - """ unqiueness constraint """ + """unqiueness constraint""" unique_together = ("user", "year") def get_remote_id(self): - """ put the year in the path """ + """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 """ + """the books you've read this year""" return ( self.user.readthrough_set.filter(finish_date__year__gte=self.year) .order_by("-finish_date") @@ -332,7 +366,7 @@ class AnnualGoal(BookWyrmModel): @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, @@ -342,12 +376,12 @@ class AnnualGoal(BookWyrmModel): @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() @@ -355,7 +389,7 @@ class AnnualGoal(BookWyrmModel): @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) @@ -365,7 +399,7 @@ def set_remote_server(user_id): 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) except FederatedServer.DoesNotExist: @@ -394,7 +428,7 @@ def get_or_create_remote_server(domain): @app.task def get_remote_reviews(outbox): - """ ingest reviews by a new remote bookwyrm user """ + """ingest reviews by a new remote bookwyrm user""" outbox_page = outbox + "?page=true&type=Review" data = get_data(outbox_page) diff --git a/bookwyrm/redis_store.py b/bookwyrm/redis_store.py new file mode 100644 index 00000000..259bc4fd --- /dev/null +++ b/bookwyrm/redis_store.py @@ -0,0 +1,86 @@ +""" access the activity stores stored in redis """ +from abc import ABC, abstractmethod +import redis + +from bookwyrm import settings + +r = redis.Redis( + host=settings.REDIS_ACTIVITY_HOST, port=settings.REDIS_ACTIVITY_PORT, db=0 +) + + +class RedisStore(ABC): + """sets of ranked, related objects, like statuses for a user's feed""" + + max_length = settings.MAX_STREAM_LENGTH + + def get_value(self, obj): + """the object and rank""" + return {obj.id: self.get_rank(obj)} + + def add_object_to_related_stores(self, obj, execute=True): + """add an object to all suitable stores""" + value = self.get_value(obj) + # we want to do this as a bulk operation, hence "pipeline" + pipeline = r.pipeline() + for store in self.get_stores_for_object(obj): + # add the status to the feed + pipeline.zadd(store, value) + # trim the store + pipeline.zremrangebyrank(store, 0, -1 * self.max_length) + if not execute: + return pipeline + # and go! + return pipeline.execute() + + def remove_object_from_related_stores(self, obj): + """remove an object from all stores""" + pipeline = r.pipeline() + for store in self.get_stores_for_object(obj): + pipeline.zrem(store, -1, obj.id) + pipeline.execute() + + def bulk_add_objects_to_store(self, objs, store): + """add a list of objects to a given store""" + pipeline = r.pipeline() + for obj in objs[: self.max_length]: + pipeline.zadd(store, self.get_value(obj)) + if objs: + pipeline.zremrangebyrank(store, 0, -1 * self.max_length) + pipeline.execute() + + def bulk_remove_objects_from_store(self, objs, store): + """remoev a list of objects from a given store""" + pipeline = r.pipeline() + for obj in objs[: self.max_length]: + pipeline.zrem(store, -1, obj.id) + pipeline.execute() + + def get_store(self, store): # pylint: disable=no-self-use + """load the values in a store""" + return r.zrevrange(store, 0, -1) + + def populate_store(self, store): + """go from zero to a store""" + pipeline = r.pipeline() + queryset = self.get_objects_for_store(store) + + for obj in queryset[: self.max_length]: + pipeline.zadd(store, self.get_value(obj)) + + # only trim the store if objects were added + if queryset.exists(): + pipeline.zremrangebyrank(store, 0, -1 * self.max_length) + pipeline.execute() + + @abstractmethod + def get_objects_for_store(self, store): + """a queryset of what should go in a store, used for populating it""" + + @abstractmethod + def get_stores_for_object(self, obj): + """the stores that an object belongs in""" + + @abstractmethod + def get_rank(self, obj): + """how to rank an object""" diff --git a/bookwyrm/sanitize_html.py b/bookwyrm/sanitize_html.py index 2a630f83..0be64c58 100644 --- a/bookwyrm/sanitize_html.py +++ b/bookwyrm/sanitize_html.py @@ -3,7 +3,7 @@ from html.parser import HTMLParser class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method - """ Removes any html that isn't allowed_tagsed from a block """ + """Removes any html that isn't allowed_tagsed from a block""" def __init__(self): HTMLParser.__init__(self) @@ -28,7 +28,7 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method 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.tag_stack.append(tag) @@ -36,7 +36,7 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method 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", "")) return @@ -51,11 +51,11 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method self.output.append(("tag", "" % tag)) def handle_data(self, data): - """ extract the answer, if we're in an answer tag """ + """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: diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 845f81c4..b679e2d4 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -24,7 +24,8 @@ 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_USE_TLS = env.bool("EMAIL_USE_TLS", True) +EMAIL_USE_SSL = env.bool("EMAIL_USE_SSL", False) DEFAULT_FROM_EMAIL = "admin@{:s}".format(env("DOMAIN")) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -33,6 +34,8 @@ LOCALE_PATHS = [ os.path.join(BASE_DIR, "locale"), ] +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ @@ -97,6 +100,7 @@ WSGI_APPLICATION = "bookwyrm.wsgi.application" # redis/activity streams settings REDIS_ACTIVITY_HOST = env("REDIS_ACTIVITY_HOST", "localhost") REDIS_ACTIVITY_PORT = env("REDIS_ACTIVITY_PORT", 6379) +REDIS_ACTIVITY_PASSWORD = env("REDIS_ACTIVITY_PASSWORD", None) MAX_STREAM_LENGTH = int(env("MAX_STREAM_LENGTH", 200)) STREAMS = ["home", "local", "federated"] @@ -151,7 +155,7 @@ LANGUAGES = [ ("de-de", _("German")), ("es", _("Spanish")), ("fr-fr", _("French")), - ("zh-cn", _("Simplified Chinese")), + ("zh-hans", _("Simplified Chinese")), ] @@ -165,7 +169,7 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/2.0/howto/static-files/ +# https://docs.djangoproject.com/en/3.1/howto/static-files/ PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) STATIC_URL = "/static/" diff --git a/bookwyrm/signatures.py b/bookwyrm/signatures.py index 80cbfdc7..5488cf9b 100644 --- a/bookwyrm/signatures.py +++ b/bookwyrm/signatures.py @@ -13,7 +13,7 @@ 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") @@ -23,7 +23,7 @@ def create_key_pair(): 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, @@ -44,14 +44,14 @@ def make_signature(sender, destination, date, digest): def make_digest(data): - """ creates a message digest for signing """ + """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 """ + """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 @@ -66,7 +66,7 @@ def verify_digest(request): class Signature: - """ read and validate incoming signatures """ + """read and validate incoming signatures""" def __init__(self, key_id, headers, signature): self.key_id = key_id @@ -75,7 +75,7 @@ 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) @@ -92,7 +92,7 @@ class Signature: return cls(key_id, headers, signature) def verify(self, public_key, request): - """ verify rsa signature """ + """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) @@ -118,7 +118,7 @@ class Signature: def http_date_age(datestr): - """ age of a signature in seconds """ + """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/css/format.css b/bookwyrm/static/css/bookwyrm.css similarity index 52% rename from bookwyrm/static/css/format.css rename to bookwyrm/static/css/bookwyrm.css index a01aff82..9e74d69f 100644 --- a/bookwyrm/static/css/format.css +++ b/bookwyrm/static/css/bookwyrm.css @@ -1,9 +1,13 @@ html { scroll-behavior: smooth; - scroll-padding-top: 20%; } -/* --- --- */ +body { + min-height: 100vh; + display: flex; + flex-direction: column; +} + .image { overflow: hidden; } @@ -25,17 +29,42 @@ html { min-width: 75% !important; } -/* --- "disabled" for non-buttons --- */ -.is-disabled { - background-color: #dbdbdb; - border-color: #dbdbdb; - box-shadow: none; - color: #7a7a7a; - opacity: 0.5; - cursor: not-allowed; +/** Utilities not covered by Bulma + ******************************************************************************/ + +@media only screen and (max-width: 768px) { + .is-sr-only-mobile { + border: none !important; + clip: rect(0, 0, 0, 0) !important; + height: 0.01em !important; + overflow: hidden !important; + padding: 0 !important; + position: absolute !important; + white-space: nowrap !important; + width: 0.01em !important; + } + + .m-0-mobile { + margin: 0 !important; + } } -/* --- SHELVING --- */ +.button.is-transparent { + background-color: transparent; +} + +.card.is-stretchable { + display: flex; + flex-direction: column; + height: 100%; +} + +.card.is-stretchable .card-content { + flex-grow: 1; +} + +/** Shelving + ******************************************************************************/ /** @todo Replace icons with SVG symbols. @see https://www.youtube.com/watch?v=9xXBYcWgCHA */ @@ -45,7 +74,9 @@ html { margin-left: 0.5em; } -/* --- TOGGLES --- */ +/** Toggles + ******************************************************************************/ + .toggle-button[aria-pressed=true], .toggle-button[aria-pressed=true]:hover { background-color: hsl(171, 100%, 41%); @@ -57,12 +88,8 @@ html { display: none; } -.hidden { - display: none !important; -} - -.hidden.transition-y, -.hidden.transition-x { +.transition-x.is-hidden, +.transition-y.is-hidden { display: block !important; visibility: hidden !important; height: 0; @@ -71,16 +98,18 @@ html { padding: 0; } +.transition-x, .transition-y { - transition-property: height, margin-top, margin-bottom, padding-top, padding-bottom; transition-duration: 0.5s; transition-timing-function: ease; } .transition-x { transition-property: width, margin-left, margin-right, padding-left, padding-right; - transition-duration: 0.5s; - transition-timing-function: ease; +} + +.transition-y { + transition-property: height, margin-top, margin-bottom, padding-top, padding-bottom; } @media (prefers-reduced-motion: reduce) { @@ -90,6 +119,13 @@ html { } } +/** Stars + ******************************************************************************/ + +.stars { + white-space: nowrap; +} + /** Stars in a review form * * Specificity makes hovering taking over checked inputs. @@ -121,7 +157,9 @@ html { content: '\e9d7'; } -/* --- BOOK COVERS --- */ +/** Book covers + ******************************************************************************/ + .cover-container { height: 250px; width: max-content; @@ -186,7 +224,9 @@ html { padding: 0.1em; } -/* --- AVATAR --- */ +/** Avatars + ******************************************************************************/ + .avatar { vertical-align: middle; display: inline; @@ -202,25 +242,107 @@ html { min-height: 96px; } -/* --- QUOTES --- */ -.quote blockquote { +/** Statuses: Quotes + * + * \e906: icon-quote-open + * \e905: icon-quote-close + * + * The `content` class on the blockquote allows to apply styles to markdown + * generated HTML in the quote: https://bulma.io/documentation/elements/content/ + * + * ```html + *
+ *
+ * User generated quote in markdown… + *
+ * + *

Book Title by Author

+ *
+ * ``` + ******************************************************************************/ + +.quote > blockquote { position: relative; padding-left: 2em; } -.quote blockquote::before, -.quote blockquote::after { +.quote > blockquote::before, +.quote > blockquote::after { font-family: 'icomoon'; position: absolute; } -.quote blockquote::before { +.quote > blockquote::before { content: "\e906"; top: 0; left: 0; } -.quote blockquote::after { +.quote > blockquote::after { content: "\e905"; right: 0; } + +/* States + ******************************************************************************/ + +/* "disabled" for non-buttons */ + +.is-disabled { + background-color: #dbdbdb; + border-color: #dbdbdb; + box-shadow: none; + color: #7a7a7a; + opacity: 0.5; + cursor: not-allowed; +} + +/* Book preview table + ******************************************************************************/ + +.book-preview td { + vertical-align: middle; +} + +@media only screen and (max-width: 768px) { + table.is-mobile, + table.is-mobile tbody { + display: block; + } + + table.is-mobile tr { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + border-top: 1px solid #dbdbdb; + } + + table.is-mobile td { + display: block; + box-sizing: border-box; + flex: 1 0 100%; + order: 2; + border-bottom: 0; + } + + table.is-mobile td.book-preview-top-row { + order: 1; + flex-basis: auto; + } + + table.is-mobile td[data-title]:not(:empty)::before { + content: attr(data-title); + display: block; + font-size: 0.75em; + font-weight: bold; + } + + table.is-mobile td:empty { + padding: 0; + } + + table.is-mobile th, + table.is-mobile thead { + display: none; + } +} diff --git a/bookwyrm/static/css/bulma.css.map b/bookwyrm/static/css/vendor/bulma.css.map similarity index 100% rename from bookwyrm/static/css/bulma.css.map rename to bookwyrm/static/css/vendor/bulma.css.map diff --git a/bookwyrm/static/css/bulma.min.css b/bookwyrm/static/css/vendor/bulma.min.css similarity index 100% rename from bookwyrm/static/css/bulma.min.css rename to bookwyrm/static/css/vendor/bulma.min.css diff --git a/bookwyrm/static/css/icons.css b/bookwyrm/static/css/vendor/icons.css similarity index 86% rename from bookwyrm/static/css/icons.css rename to bookwyrm/static/css/vendor/icons.css index 9915ecd1..c78af145 100644 --- a/bookwyrm/static/css/icons.css +++ b/bookwyrm/static/css/vendor/icons.css @@ -1,10 +1,13 @@ + +/** @todo Replace icons with SVG symbols. + @see https://www.youtube.com/watch?v=9xXBYcWgCHA */ @font-face { font-family: 'icomoon'; - src: url('fonts/icomoon.eot?n5x55'); - src: url('fonts/icomoon.eot?n5x55#iefix') format('embedded-opentype'), - url('fonts/icomoon.ttf?n5x55') format('truetype'), - url('fonts/icomoon.woff?n5x55') format('woff'), - url('fonts/icomoon.svg?n5x55#icomoon') format('svg'); + src: url('../fonts/icomoon.eot?n5x55'); + src: url('../fonts/icomoon.eot?n5x55#iefix') format('embedded-opentype'), + url('../fonts/icomoon.ttf?n5x55') format('truetype'), + url('../fonts/icomoon.woff?n5x55') format('woff'), + url('../fonts/icomoon.svg?n5x55#icomoon') format('svg'); font-weight: normal; font-style: normal; font-display: block; diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js new file mode 100644 index 00000000..485daf15 --- /dev/null +++ b/bookwyrm/static/js/bookwyrm.js @@ -0,0 +1,285 @@ +/* exported BookWyrm */ +/* globals TabGroup */ + +let BookWyrm = new class { + constructor() { + this.initOnDOMLoaded(); + this.initReccuringTasks(); + this.initEventListeners(); + } + + initEventListeners() { + document.querySelectorAll('[data-controls]') + .forEach(button => button.addEventListener( + 'click', + this.toggleAction.bind(this)) + ); + + document.querySelectorAll('.interaction') + .forEach(button => button.addEventListener( + 'submit', + this.interact.bind(this)) + ); + + document.querySelectorAll('.hidden-form input') + .forEach(button => button.addEventListener( + 'change', + this.revealForm.bind(this)) + ); + + document.querySelectorAll('[data-back]') + .forEach(button => button.addEventListener( + 'click', + this.back) + ); + } + + /** + * Execute code once the DOM is loaded. + */ + initOnDOMLoaded() { + window.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('.tab-group') + .forEach(tabs => new TabGroup(tabs)); + }); + } + + /** + * Execute recurring tasks. + */ + initReccuringTasks() { + // Polling + document.querySelectorAll('[data-poll]') + .forEach(liveArea => this.polling(liveArea)); + } + + /** + * Go back in browser history. + * + * @param {Event} event + * @return {undefined} + */ + back(event) { + event.preventDefault(); + history.back(); + } + + /** + * Update a counter with recurring requests to the API + * The delay is slightly randomized and increased on each cycle. + * + * @param {Object} counter - DOM node + * @param {int} delay - frequency for polling in ms + * @return {undefined} + */ + polling(counter, delay) { + const bookwyrm = this; + + delay = delay || 10000; + delay += (Math.random() * 1000); + + setTimeout(function() { + fetch('/api/updates/' + counter.dataset.poll) + .then(response => response.json()) + .then(data => bookwyrm.updateCountElement(counter, data)); + + bookwyrm.polling(counter, delay * 1.25); + }, delay, counter); + } + + /** + * Update a counter. + * + * @param {object} counter - DOM node + * @param {object} data - json formatted response from a fetch + * @return {undefined} + */ + updateCountElement(counter, data) { + const currentCount = counter.innerText; + const count = data.count; + + if (count != currentCount) { + this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-hidden', count < 1); + counter.innerText = count; + } + } + + /** + * Toggle form. + * + * @param {Event} event + * @return {undefined} + */ + revealForm(event) { + let trigger = event.currentTarget; + let hidden = trigger.closest('.hidden-form').querySelectorAll('.is-hidden')[0]; + + this.addRemoveClass(hidden, 'is-hidden', !hidden); + } + + /** + * Execute actions on targets based on triggers. + * + * @param {Event} event + * @return {undefined} + */ + toggleAction(event) { + let trigger = event.currentTarget; + let pressed = trigger.getAttribute('aria-pressed') === 'false'; + let targetId = trigger.dataset.controls; + + // Toggle pressed status on all triggers controlling the same target. + document.querySelectorAll('[data-controls="' + targetId + '"]') + .forEach(otherTrigger => otherTrigger.setAttribute( + 'aria-pressed', + otherTrigger.getAttribute('aria-pressed') === 'false' + )); + + // @todo Find a better way to handle the exception. + if (targetId && ! trigger.classList.contains('pulldown-menu')) { + let target = document.getElementById(targetId); + + this.addRemoveClass(target, 'is-hidden', !pressed); + this.addRemoveClass(target, 'is-active', pressed); + } + + // Show/hide pulldown-menus. + if (trigger.classList.contains('pulldown-menu')) { + this.toggleMenu(trigger, targetId); + } + + // Show/hide container. + let container = document.getElementById('hide-' + targetId); + + if (container) { + this.toggleContainer(container, pressed); + } + + // Check checkbox, if appropriate. + let checkbox = trigger.dataset.controlsCheckbox; + + if (checkbox) { + this.toggleCheckbox(checkbox, pressed); + } + + // Set focus, if appropriate. + let focus = trigger.dataset.focusTarget; + + if (focus) { + this.toggleFocus(focus); + } + } + + /** + * Show or hide menus. + * + * @param {Event} event + * @return {undefined} + */ + toggleMenu(trigger, targetId) { + let expanded = trigger.getAttribute('aria-expanded') == 'false'; + + trigger.setAttribute('aria-expanded', expanded); + + if (targetId) { + let target = document.getElementById(targetId); + + this.addRemoveClass(target, 'is-active', expanded); + } + } + + /** + * Show or hide generic containers. + * + * @param {object} container - DOM node + * @param {boolean} pressed - Is the trigger pressed? + * @return {undefined} + */ + toggleContainer(container, pressed) { + this.addRemoveClass(container, 'is-hidden', pressed); + } + + /** + * Check or uncheck a checbox. + * + * @param {object} checkbox - DOM node + * @param {boolean} pressed - Is the trigger pressed? + * @return {undefined} + */ + toggleCheckbox(checkbox, pressed) { + document.getElementById(checkbox).checked = !!pressed; + } + + /** + * Give the focus to an element. + * Only move the focus based on user interactions. + * + * @param {string} nodeId - ID of the DOM node to focus (button, link…) + * @return {undefined} + */ + toggleFocus(nodeId) { + let node = document.getElementById(nodeId); + + node.focus(); + + setTimeout(function() { + node.selectionStart = node.selectionEnd = 10000; + }, 0); + } + + /** + * Make a request and update the UI accordingly. + * This function is used for boosts, favourites, follows and unfollows. + * + * @param {Event} event + * @return {undefined} + */ + interact(event) { + event.preventDefault(); + + const bookwyrm = this; + const form = event.currentTarget; + const relatedforms = document.querySelectorAll(`.${form.dataset.id}`); + + // Toggle class on all related forms. + relatedforms.forEach(relatedForm => bookwyrm.addRemoveClass( + relatedForm, + 'is-hidden', + relatedForm.className.indexOf('is-hidden') == -1 + )); + + this.ajaxPost(form).catch(error => { + // @todo Display a notification in the UI instead. + console.warn('Request failed:', error); + }); + } + + /** + * Submit a form using POST. + * + * @param {object} form - Form to be submitted + * @return {Promise} + */ + ajaxPost(form) { + return fetch(form.action, { + method : "POST", + body: new FormData(form) + }); + } + + /** + * Add or remove a class based on a boolean condition. + * + * @param {object} node - DOM node to change class on + * @param {string} classname - Name of the class + * @param {boolean} add - Add? + * @return {undefined} + */ + addRemoveClass(node, classname, add) { + if (add) { + node.classList.add(classname); + } else { + node.classList.remove(classname); + } + } +} diff --git a/bookwyrm/static/js/check_all.js b/bookwyrm/static/js/check_all.js index 07d30a68..fd29f2cd 100644 --- a/bookwyrm/static/js/check_all.js +++ b/bookwyrm/static/js/check_all.js @@ -1,17 +1,34 @@ -/* exported toggleAllCheckboxes */ -/** - * 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; +(function() { + 'use strict'; + + /** + * Toggle all descendant checkboxes of a target. + * + * Use `data-target="ID_OF_TARGET"` on the node on which the event is listened + * to (checkbox, button, link…), where_ID_OF_TARGET_ should be the ID of an + * ancestor for the checkboxes. + * + * @example + * + * @param {Event} event + * @return {undefined} + */ + function toggleAllCheckboxes(event) { + const mainCheckbox = event.target; + + document + .querySelectorAll(`#${mainCheckbox.dataset.target} [type="checkbox"]`) + .forEach(checkbox => checkbox.checked = mainCheckbox.checked); + } document - .querySelectorAll(`#${mainCheckbox.dataset.target} [type="checkbox"]`) - .forEach(checkbox => {checkbox.checked = mainCheckbox.checked;}); -} + .querySelectorAll('[data-action="toggle-all"]') + .forEach(input => { + input.addEventListener('change', toggleAllCheckboxes); + }); +})(); diff --git a/bookwyrm/static/js/localstorage.js b/bookwyrm/static/js/localstorage.js index aa79ee30..05955779 100644 --- a/bookwyrm/static/js/localstorage.js +++ b/bookwyrm/static/js/localstorage.js @@ -1,20 +1,43 @@ -/* exported updateDisplay */ -/* globals addRemoveClass */ +/* exported LocalStorageTools */ +/* globals BookWyrm */ -// 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); +let LocalStorageTools = new class { + constructor() { + document.querySelectorAll('[data-hide]') + .forEach(t => this.setDisplay(t)); - 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); + document.querySelectorAll('.set-display') + .forEach(t => t.addEventListener('click', this.updateDisplay.bind(this))); + } + + /** + * Update localStorage, then display content based on keys in localStorage. + * + * @param {Event} event + * @return {undefined} + */ + updateDisplay(event) { + // used in set reading goal + let key = event.target.dataset.id; + let value = event.target.dataset.value; + + window.localStorage.setItem(key, value); + + document.querySelectorAll('[data-hide="' + key + '"]') + .forEach(node => this.setDisplay(node)); + } + + /** + * Toggle display of a DOM node based on its value in the localStorage. + * + * @param {object} node - DOM node to toggle. + * @return {undefined} + */ + setDisplay(node) { + // used in set reading goal + let key = node.dataset.hide; + let value = window.localStorage.getItem(key); + + BookWyrm.addRemoveClass(node, 'is-hidden', value); + } } diff --git a/bookwyrm/static/js/shared.js b/bookwyrm/static/js/shared.js deleted file mode 100644 index 7a198619..00000000 --- a/bookwyrm/static/js/shared.js +++ /dev/null @@ -1,169 +0,0 @@ -/* globals setDisplay TabGroup toggleAllCheckboxes updateDisplay */ - -// set up javascript listeners -window.onload = function() { - // buttons that display or hide content - document.querySelectorAll('[data-controls]') - .forEach(t => t.onclick = toggleAction); - - // javascript interactions (boost/fav) - Array.from(document.getElementsByClassName('interaction')) - .forEach(t => t.onsubmit = interact); - - // handle aria settings on menus - Array.from(document.getElementsByClassName('pulldown-menu')) - .forEach(t => t.onclick = toggleMenu); - - // hidden submit button in a form - document.querySelectorAll('.hidden-form input') - .forEach(t => t.onchange = revealForm); - - // polling - document.querySelectorAll('[data-poll]') - .forEach(el => polling(el)); - - // 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) { - e.preventDefault(); - history.back(); -} - -function polling(el, delay) { - delay = delay || 10000; - delay += (Math.random() * 1000); - setTimeout(function() { - fetch('/api/updates/' + el.getAttribute('data-poll')) - .then(response => response.json()) - .then(data => updateCountElement(el, data)); - polling(el, delay * 1.25); - }, delay, el); -} - -function updateCountElement(el, data) { - const currentCount = el.innerText; - const count = data.count; - if (count != currentCount) { - addRemoveClass(el.closest('[data-poll-wrapper]'), 'hidden', count < 1); - el.innerText = count; - } -} - - -function revealForm(e) { - var hidden = e.currentTarget.closest('.hidden-form').getElementsByClassName('hidden')[0]; - if (hidden) { - removeClass(hidden, 'hidden'); - } -} - - -function toggleAction(e) { - var el = e.currentTarget; - var pressed = el.getAttribute('aria-pressed') == 'false'; - - var targetId = el.getAttribute('data-controls'); - document.querySelectorAll('[data-controls="' + targetId + '"]') - .forEach(t => t.setAttribute('aria-pressed', (t.getAttribute('aria-pressed') == 'false'))); - - if (targetId) { - var target = document.getElementById(targetId); - addRemoveClass(target, 'hidden', !pressed); - addRemoveClass(target, 'is-active', pressed); - } - - // show/hide container - var container = document.getElementById('hide-' + targetId); - if (container) { - addRemoveClass(container, 'hidden', pressed); - } - - // set checkbox, if appropriate - var checkbox = el.getAttribute('data-controls-checkbox'); - if (checkbox) { - document.getElementById(checkbox).checked = !!pressed; - } - - // set focus, if appropriate - var focus = el.getAttribute('data-focus-target'); - if (focus) { - var focusEl = document.getElementById(focus); - focusEl.focus(); - setTimeout(function(){ focusEl.selectionStart = focusEl.selectionEnd = 10000; }, 0); - } -} - -function interact(e) { - e.preventDefault(); - ajaxPost(e.target); - var identifier = e.target.getAttribute('data-id'); - Array.from(document.getElementsByClassName(identifier)) - .forEach(t => addRemoveClass(t, 'hidden', t.className.indexOf('hidden') == -1)); -} - -function toggleMenu(e) { - var el = e.currentTarget; - var expanded = el.getAttribute('aria-expanded') == 'false'; - el.setAttribute('aria-expanded', expanded); - var targetId = el.getAttribute('data-controls'); - if (targetId) { - var target = document.getElementById(targetId); - addRemoveClass(target, 'is-active', expanded); - } -} - -function ajaxPost(form) { - fetch(form.action, { - method : "POST", - body: new FormData(form) - }); -} - -function addRemoveClass(el, classname, bool) { - if (bool) { - addClass(el, classname); - } else { - removeClass(el, classname); - } -} - -function addClass(el, classname) { - var classes = el.className.split(' '); - if (classes.indexOf(classname) > -1) { - return; - } - el.className = classes.concat(classname).join(' '); -} - -function removeClass(el, className) { - var classes = []; - if (el.className) { - classes = el.className.split(' '); - } - const idx = classes.indexOf(className); - if (idx > -1) { - classes.splice(idx, 1); - } - el.className = classes.join(' '); -} diff --git a/bookwyrm/static/js/tabs.js b/bookwyrm/static/js/vendor/tabs.js similarity index 100% rename from bookwyrm/static/js/tabs.js rename to bookwyrm/static/js/vendor/tabs.js diff --git a/bookwyrm/status.py b/bookwyrm/status.py index 793bd742..09fbdc06 100644 --- a/bookwyrm/status.py +++ b/bookwyrm/status.py @@ -6,7 +6,7 @@ from bookwyrm.sanitize_html import InputHtmlParser def create_generated_note(user, content, mention_books=None, privacy="public"): - """ a note created by the app about user activity """ + """a note created by the app about user activity""" # sanitize input html parser = InputHtmlParser() parser.feed(content) diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index b91cebba..97f105bf 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -6,24 +6,36 @@ {% block title %}{{ book.title }}{% endblock %} {% block content %} -
+{% with user_authenticated=request.user.is_authenticated can_edit_book=perms.bookwyrm.edit_book %} +

- {{ book.title }}{% if book.subtitle %}: - {{ book.subtitle }}{% endif %} + + {{ book.title }}{% if book.subtitle %}: + {{ book.subtitle }} + {% endif %} + + {% if book.series %} - ({{ book.series }}{% if book.series_number %} #{{ book.series_number }}{% endif %})
+ + + + + ({{ book.series }} + {% if book.series_number %} #{{ book.series_number }}{% endif %}) + +
{% endif %}

{% if book.authors %}

- {% trans "by" %} {% include 'snippets/authors.html' with book=book %} + {% trans "by" %} {% include 'snippets/authors.html' with book=book %}

{% endif %}
- {% if request.user.is_authenticated and perms.bookwyrm.edit_book %} + {% if user_authenticated and can_edit_book %} - {% if request.user.is_authenticated and not book.cover %} + {% if user_authenticated and not book.cover %}
{% 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" %} @@ -55,31 +67,16 @@
{% endif %} -
-
- {% if book.isbn_13 %} -
-
{% trans "ISBN:" %}
-
{{ book.isbn_13 }}
+
+ {% with book=book %} +
+ {% include 'book/publisher_info.html' %}
- {% endif %} - {% if book.oclc_number %} -
-
{% trans "OCLC Number:" %}
-
{{ book.oclc_number }}
+
+ {% include 'book/book_identifiers.html' %}
- {% endif %} - - {% if book.asin %} -
-
{% trans "ASIN:" %}
-
{{ book.asin }}
-
- {% endif %} -
- - {% include 'book/publisher_info.html' with book=book %} + {% endwith %} {% if book.openlibrary_key %}

{% trans "View on OpenLibrary" %}

@@ -89,18 +86,35 @@
-

+

+ + {# @todo Is it possible to not hard-code the value? #} + + + {% include 'snippets/stars.html' with rating=rating %} - {% blocktrans count counter=review_count %}({{ review_count }} review){% plural %}({{ review_count }} reviews){% endblocktrans %} + + {% blocktrans count counter=review_count trimmed %} + ({{ review_count }} review) + {% plural %} + ({{ review_count }} reviews) + {% endblocktrans %}

- {% include 'snippets/trimmed_text.html' with full=book|book_description %} + {% with full=book|book_description itemprop='abstract' %} + {% include 'snippets/trimmed_text.html' %} + {% endwith %} - {% if request.user.is_authenticated and perms.bookwyrm.edit_book and not book|book_description %} + {% if user_authenticated and can_edit_book and not book|book_description %} {% trans 'Add Description' as button_text %} {% include 'snippets/toggle/open_button.html' with text=button_text controls_text="add-description" controls_uid=book.id focus="id_description" hide_active=True id="hide-description" %} - -
-
- {% for review in reviews %} -
- {% include 'snippets/status/status.html' with status=review hide_book=True depth=1 %} -
- {% endfor %} +
+ {% if request.user.is_authenticated %} + + {% endif %} -
- {% for rating in ratings %} -
-
-
{% include 'snippets/avatar.html' with user=rating.user %}
-
- -
-

{% trans "rated it" %}

- {% include 'snippets/stars.html' with rating=rating.rating %} -
- -
+ {% for review in statuses %} +
+ {% with status=review hide_book=True depth=1 %} + {% include 'snippets/status/status.html' %} + {% endwith %}
-
{% endfor %} -
-
- {% include 'snippets/pagination.html' with page=reviews path=book.local_path anchor="#reviews" %} + +
+ {% for rating in ratings %} + {% with user=rating.user %} +
+
+
+ {% include 'snippets/avatar.html' %} +
+ +
+ +
+

{% trans "rated it" %}

+ + {% include 'snippets/stars.html' with rating=rating.rating %} +
+ +
+
+
+ {% endwith %} + {% endfor %} +
+
+ {% include 'snippets/pagination.html' with page=statuses path=request.path anchor="#reviews" %} +
- +{% endwith %} {% endblock %} {% block scripts %} - + {% endblock %} diff --git a/bookwyrm/templates/book/book_identifiers.html b/bookwyrm/templates/book/book_identifiers.html new file mode 100644 index 00000000..d71ea409 --- /dev/null +++ b/bookwyrm/templates/book/book_identifiers.html @@ -0,0 +1,27 @@ +{% spaceless %} + +{% load i18n %} + +
+ {% 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 }}
+
+ {% endif %} +
+{% endspaceless %} diff --git a/bookwyrm/templates/book/edit_book.html b/bookwyrm/templates/book/edit_book.html index a9ce651e..1702cf5d 100644 --- a/bookwyrm/templates/book/edit_book.html +++ b/bookwyrm/templates/book/edit_book.html @@ -88,12 +88,18 @@

{% trans "Metadata" %}

-

{{ form.title }}

+

+ + +

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

{{ error | escape }}

{% endfor %} -

{{ form.subtitle }}

+

+ + +

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

{{ error | escape }}

{% endfor %} @@ -103,7 +109,10 @@

{{ error | escape }}

{% endfor %} -

{{ form.series }}

+

+ + +

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

{{ error | escape }}

{% endfor %} @@ -124,7 +133,7 @@

- +

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

{{ error | escape }}

@@ -132,7 +141,7 @@

- +

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

{{ error | escape }}

diff --git a/bookwyrm/templates/book/editions.html b/bookwyrm/templates/book/editions.html index 91259465..70f067f7 100644 --- a/bookwyrm/templates/book/editions.html +++ b/bookwyrm/templates/book/editions.html @@ -25,7 +25,18 @@ {{ book.title }} - {% include 'book/publisher_info.html' with book=book %} + + {% with book=book %} +
+
+ {% include 'book/publisher_info.html' %} +
+ +
+ {% include 'book/book_identifiers.html' %} +
+
+ {% endwith %}
{% include 'snippets/shelve_button/shelve_button.html' with book=book switch_mode=True %} diff --git a/bookwyrm/templates/book/publisher_info.html b/bookwyrm/templates/book/publisher_info.html index 0ab35401..b7975a62 100644 --- a/bookwyrm/templates/book/publisher_info.html +++ b/bookwyrm/templates/book/publisher_info.html @@ -1,24 +1,70 @@ +{% spaceless %} + {% load i18n %} +{% load humanize %} +

- {% 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 %} + {% with format=book.physical_format pages=book.pages %} + {% if format %} + {% comment %} + @todo The bookFormat property is limited to a list of values whereas the book edition is free text. + @see https://schema.org/bookFormat + {% endcomment %} + + {% endif %} + + {% if pages %} + + {% endif %} + + {% if format and not pages %} + {% blocktrans %}{{ format }}{% endblocktrans %} + {% elif format and pages %} + {% blocktrans %}{{ format }}, {{ pages }} pages{% endblocktrans %} + {% elif pages %} + {% blocktrans %}{{ pages }} pages{% endblocktrans %} + {% endif %} + {% endwith %}

+ {% if book.languages %} -

- {% blocktrans with languages=book.languages|join:", " %}{{ languages }} language{% endblocktrans %} -

+ {% for language in book.languages %} + + {% endfor %} + +

+ {% with languages=book.languages|join:", " %} + {% blocktrans %}{{ languages }} language{% endblocktrans %} + {% endwith %} +

{% 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 %} + {% with date=book.published_date|naturalday publisher=book.publishers|join:', ' %} + {% if date or book.first_published_date %} + + {% endif %} + + {% comment %} + @todo The publisher property needs to be an Organization or a Person. We’ll be using Thing which is the more generic ancestor. + @see https://schema.org/Publisher + {% endcomment %} + {% if book.publishers %} + {% for publisher in book.publishers %} + + {% endfor %} + {% endif %} + + {% if date and publisher %} + {% blocktrans %}Published {{ date }} by {{ publisher }}.{% endblocktrans %} + {% elif date %} + {% blocktrans %}Published {{ date }}{% endblocktrans %} + {% elif publisher %} + {% blocktrans %}Published by {{ publisher }}.{% endblocktrans %} + {% endif %} + {% endwith %}

+{% endspaceless %} diff --git a/bookwyrm/templates/components/dropdown.html b/bookwyrm/templates/components/dropdown.html index 72582ddc..96dce823 100644 --- a/bookwyrm/templates/components/dropdown.html +++ b/bookwyrm/templates/components/dropdown.html @@ -1,13 +1,34 @@ +{% spaceless %} {% load bookwyrm_tags %} + {% with 0|uuid as uuid %} - {% endwith %} +{% endspaceless %} diff --git a/bookwyrm/templates/components/inline_form.html b/bookwyrm/templates/components/inline_form.html index 40915a92..0b2c1300 100644 --- a/bookwyrm/templates/components/inline_form.html +++ b/bookwyrm/templates/components/inline_form.html @@ -1,5 +1,5 @@ {% load i18n %} -
{% endblock %} diff --git a/bookwyrm/templates/lists/lists.html b/bookwyrm/templates/lists/lists.html index 27e56f11..c7d789d0 100644 --- a/bookwyrm/templates/lists/lists.html +++ b/bookwyrm/templates/lists/lists.html @@ -15,10 +15,12 @@ {% endif %}
+ {% if request.user.is_authenticated %}
{% trans "Create List" as button_text %} {% include 'snippets/toggle/open_button.html' with controls_text="create-list" icon="plus" text=button_text focus="create-list-header" %}
+ {% endif %}
diff --git a/bookwyrm/templates/moderation/report.html b/bookwyrm/templates/moderation/report.html index a231c41c..934799e3 100644 --- a/bookwyrm/templates/moderation/report.html +++ b/bookwyrm/templates/moderation/report.html @@ -1,5 +1,6 @@ {% extends 'settings/admin_layout.html' %} {% load i18n %} +{% load bookwyrm_tags %} {% load humanize %} {% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %} @@ -14,23 +15,9 @@ {% include 'moderation/report_preview.html' with report=report %}
-
-

{% trans "Actions" %}

-

{% trans "View user profile" %}

-
-

- {% trans "Send direct message" %} -

- - {% csrf_token %} - {% if report.user.is_active %} - - {% else %} - - {% endif %} - -
-
+{% include 'user_admin/user_info.html' with user=report.user %} + +{% include 'user_admin/user_moderation_actions.html' with user=report.user %}

{% trans "Moderator Comments" %}

@@ -64,7 +51,7 @@ {% for status in report.statuses.select_subclasses.all %}
  • {% if status.deleted %} - {% trans "Statuses has been deleted" %} + {% trans "Status has been deleted" %} {% else %} {% include 'snippets/status/status.html' with status=status moderation_mode=True %} {% endif %} diff --git a/bookwyrm/templates/moderation/report_modal.html b/bookwyrm/templates/moderation/report_modal.html index ce8408ee..0d6504ab 100644 --- a/bookwyrm/templates/moderation/report_modal.html +++ b/bookwyrm/templates/moderation/report_modal.html @@ -15,7 +15,9 @@ {% csrf_token %} +{% if status %} +{% endif %}

    {% blocktrans with site_name=site.name %}This report will be sent to {{ site_name }}'s moderators for review.{% endblocktrans %}

    diff --git a/bookwyrm/templates/moderation/reports.html b/bookwyrm/templates/moderation/reports.html index f486711f..f9d9d99b 100644 --- a/bookwyrm/templates/moderation/reports.html +++ b/bookwyrm/templates/moderation/reports.html @@ -8,6 +8,7 @@ {% trans "Reports" %} {% endif %} {% endblock %} + {% block header %} {% if server %} {% blocktrans with server_name=server.server_name %}Reports: {{ server_name }}{% endblocktrans %} @@ -29,6 +30,8 @@
  • +{% include 'user_admin/user_admin_filters.html' %} +
    {% if not reports %} {% trans "No reports found." %} diff --git a/bookwyrm/templates/notifications.html b/bookwyrm/templates/notifications.html index 7c694d78..ba0a25cd 100644 --- a/bookwyrm/templates/notifications.html +++ b/bookwyrm/templates/notifications.html @@ -123,7 +123,7 @@ {% include 'snippets/status_preview.html' with status=related_status %}
    - {{ related_status.published_date | post_date }} + {{ related_status.published_date|timesince }} {% include 'snippets/privacy-icons.html' with item=related_status %}
    diff --git a/bookwyrm/templates/search_results.html b/bookwyrm/templates/search_results.html index 6444cc18..4c9c23da 100644 --- a/bookwyrm/templates/search_results.html +++ b/bookwyrm/templates/search_results.html @@ -37,7 +37,7 @@
    {% endif %} -
    +
    {% for result_set in book_results|slice:"1:" %} {% if result_set.results %}
    diff --git a/bookwyrm/templates/settings/admin_layout.html b/bookwyrm/templates/settings/admin_layout.html index 9340da9e..4f71a228 100644 --- a/bookwyrm/templates/settings/admin_layout.html +++ b/bookwyrm/templates/settings/admin_layout.html @@ -6,7 +6,14 @@ {% block content %}
    -

    {% block header %}{% endblock %}

    +
    +
    +

    {% block header %}{% endblock %}

    +
    +
    + {% block edit-button %}{% endblock %} +
    +
    diff --git a/bookwyrm/templates/settings/edit_server.html b/bookwyrm/templates/settings/edit_server.html new file mode 100644 index 00000000..c5702c84 --- /dev/null +++ b/bookwyrm/templates/settings/edit_server.html @@ -0,0 +1,71 @@ +{% extends 'settings/admin_layout.html' %} +{% load i18n %} +{% block title %}{% trans "Add server" %}{% endblock %} + +{% block header %} +{% trans "Add server" %} +{% trans "Back to server list" %} +{% endblock %} + +{% block panel %} + +
    + +
    + +
    + {% csrf_token %} +
    +
    +
    + + + {% for error in form.server_name.errors %} +

    {{ error | escape }}

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

    {{ error | escape }}

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

    {{ error | escape }}

    + {% endfor %} +
    +
    +
    +

    + + +

    + + +
    + +{% endblock %} diff --git a/bookwyrm/templates/settings/federated_server.html b/bookwyrm/templates/settings/federated_server.html index 13715bfb..386433f3 100644 --- a/bookwyrm/templates/settings/federated_server.html +++ b/bookwyrm/templates/settings/federated_server.html @@ -1,67 +1,116 @@ {% extends 'settings/admin_layout.html' %} {% block title %}{{ server.server_name }}{% endblock %} {% load i18n %} +{% load bookwyrm_tags %} {% block header %} {{ server.server_name }} + +{% if server.status == "blocked" %}{% trans "Blocked" %} +{% endif %} + {% trans "Back to server list" %} {% endblock %} {% block panel %} +
    +
    +

    {% trans "Details" %}

    +
    +
    +
    {% trans "Software:" %}
    +
    {{ server.application_type }}
    +
    +
    +
    {% trans "Version:" %}
    +
    {{ server.application_version }}
    +
    +
    +
    {% trans "Status:" %}
    +
    {{ server.status }}
    +
    +
    +
    + +
    +

    {% trans "Activity" %}

    +
    +
    +
    {% trans "Users:" %}
    +
    + {{ users.count }} + {% if server.user_set.count %}({% trans "View all" %}){% endif %} +
    +
    +
    +
    {% trans "Reports:" %}
    +
    + {{ reports.count }} + {% if reports.count %}({% trans "View all" %}){% endif %} +
    +
    +
    +
    {% trans "Followed by us:" %}
    +
    + {{ followed_by_us.count }} +
    +
    +
    +
    {% trans "Followed by them:" %}
    +
    + {{ followed_by_them.count }} +
    +
    +
    +
    {% trans "Blocked by us:" %}
    +
    + {{ blocked_by_us.count }} +
    +
    +
    +
    +
    +
    -

    {% trans "Details" %}

    -
    -
    -
    {% trans "Software:" %}
    -
    {{ server.application_type }}
    +
    +
    +

    {% trans "Notes" %}

    -
    -
    {% trans "Version:" %}
    -
    {{ server.application_version }}
    +
    + {% trans "Edit" as button_text %} + {% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="edit-notes" %}
    -
    -
    {% trans "Status:" %}
    -
    Federated
    -
    -
    + + {% if server.notes %} +

    {{ server.notes|to_markdown|safe }}

    + {% endif %} +
    -

    {% trans "Activity" %}

    -
    -
    -
    {% trans "Users:" %}
    -
    - {{ users.count }} - {% if server.user_set.count %}({% trans "View all" %}){% endif %} -
    -
    -
    -
    {% trans "Reports:" %}
    -
    - {{ reports.count }} - {% if reports.count %}({% trans "View all" %}){% endif %} -
    -
    -
    -
    {% trans "Followed by us:" %}
    -
    - {{ followed_by_us.count }} -
    -
    -
    -
    {% trans "Followed by them:" %}
    -
    - {{ followed_by_them.count }} -
    -
    -
    -
    {% trans "Blocked by us:" %}
    -
    - {{ blocked_by_us.count }} -
    -
    -
    +

    {% trans "Actions" %}

    + {% if server.status != 'blocked' %} +
    + {% csrf_token %} + +

    {% trans "All users from this instance will be deactivated." %}

    +
    + {% else %} +
    + {% csrf_token %} + +

    {% trans "All users from this instance will be re-activated." %}

    +
    + {% endif %}
    {% endblock %} diff --git a/bookwyrm/templates/settings/federation.html b/bookwyrm/templates/settings/federation.html index 696d7a20..9eb0f8b1 100644 --- a/bookwyrm/templates/settings/federation.html +++ b/bookwyrm/templates/settings/federation.html @@ -4,8 +4,15 @@ {% block header %}{% trans "Federated Servers" %}{% endblock %} -{% block panel %} +{% block edit-button %} + + + {% trans "Add server" %} + + +{% endblock %} +{% block panel %} {% url 'settings-federation' as url %} diff --git a/bookwyrm/templates/settings/server_blocklist.html b/bookwyrm/templates/settings/server_blocklist.html new file mode 100644 index 00000000..0de49acd --- /dev/null +++ b/bookwyrm/templates/settings/server_blocklist.html @@ -0,0 +1,67 @@ +{% extends 'settings/admin_layout.html' %} +{% load i18n %} +{% block title %}{% trans "Add server" %}{% endblock %} + +{% block header %} +{% trans "Import Blocklist" %} +{% trans "Back to server list" %} +{% endblock %} + +{% block panel %} + +
    + +
    + +{% if succeeded and not failed %} +

    {% trans "Success!" %}

    +{% elif succeeded or failed %} +
    + {% if succeeded %} +

    {% trans "Successfully blocked:" %} {{ succeeded }}

    + {% endif %} +

    {% trans "Failed:" %}

    +
      + {% for item in failed %} +
    • +
      +{{ item }}
      +
      +
    • + {% endfor %} +
    +
    +{% endif %} + + + {% csrf_token %} +
    + + + +
    + + + + +{% endblock %} diff --git a/bookwyrm/templates/snippets/authors.html b/bookwyrm/templates/snippets/authors.html index dd94b471..9459b0fe 100644 --- a/bookwyrm/templates/snippets/authors.html +++ b/bookwyrm/templates/snippets/authors.html @@ -1 +1,17 @@ -{% for author in book.authors.all %}{{ author.name }}{% if not forloop.last %}, {% endif %}{% endfor %} +{% spaceless %} +{% comment %} + @todo The author property needs to be an Organization or a Person. We’ll be using Thing which is the more generic ancestor. + @see https://schema.org/Author +{% endcomment %} +{% for author in book.authors.all %} + {% if not forloop.last %}, {% endif %} +{% endfor %} +{% endspaceless %} diff --git a/bookwyrm/templates/snippets/book_cover.html b/bookwyrm/templates/snippets/book_cover.html index 0dbc3672..ce47819e 100644 --- a/bookwyrm/templates/snippets/book_cover.html +++ b/bookwyrm/templates/snippets/book_cover.html @@ -1,13 +1,29 @@ +{% spaceless %} + {% load bookwyrm_tags %} +{% load i18n %} +
    -{% if book.cover %} -{{ book.alt_text }} -{% else %} -
    - No cover -
    -

    {{ book.alt_text }}

    + {% if book.cover %} + {{ book.alt_text }} + {% else %} +
    + {% trans + +
    +

    {{ book.alt_text }}

    +
    -
    -{% endif %} + {% endif %}
    +{% endspaceless %} diff --git a/bookwyrm/templates/snippets/book_titleby.html b/bookwyrm/templates/snippets/book_titleby.html index e561a8a3..80127fb7 100644 --- a/bookwyrm/templates/snippets/book_titleby.html +++ b/bookwyrm/templates/snippets/book_titleby.html @@ -1,7 +1,8 @@ {% load i18n %} +{% load bookwyrm_tags %} {% if book.authors %} -{% blocktrans with path=book.local_path title=book.title %}{{ title }} by {% endblocktrans %}{% include 'snippets/authors.html' with book=book %} +{% blocktrans with path=book.local_path title=book|title %}{{ title }} by {% endblocktrans %}{% include 'snippets/authors.html' with book=book %} {% else %} -{{ book.title }} +{{ book|title }} {% endif %} diff --git a/bookwyrm/templates/snippets/boost_button.html b/bookwyrm/templates/snippets/boost_button.html index 3bc1b601..3a01fc82 100644 --- a/bookwyrm/templates/snippets/boost_button.html +++ b/bookwyrm/templates/snippets/boost_button.html @@ -2,20 +2,18 @@ {% load i18n %} {% with status.id|uuid as uuid %} -
    + {% csrf_token %} - -
    + {% csrf_token %} - {% endwith %} diff --git a/bookwyrm/templates/snippets/content_warning_field.html b/bookwyrm/templates/snippets/content_warning_field.html index b645a29c..51c2649b 100644 --- a/bookwyrm/templates/snippets/content_warning_field.html +++ b/bookwyrm/templates/snippets/content_warning_field.html @@ -1,5 +1,5 @@ {% load i18n %} -
    +
    {% if type == 'review' %} -
    +
    - +
    + +
    {% endif %} -
    +
    {% if type != 'reply' and type != 'direct' %} -
    + + {# Supplemental fields #} {% if type == 'quotation' %} -
    +
    {% include 'snippets/content_warning_field.html' with parent_status=status %} - +
    + +
    {% elif type == 'comment' %} -
    +
    {% active_shelf book as active_shelf %} {% if active_shelf.shelf.identifier == 'reading' and book.latest_readthrough %} @@ -58,11 +69,13 @@
    -
    - +
    +
    + +
    {% if readthrough.progress_mode == 'PG' and book.pages %} @@ -73,9 +86,12 @@ {% endif %}
    {% endif %} - + + {# bottom bar #} -
    + + +
    {% trans "Include spoiler alert" as button_text %} diff --git a/bookwyrm/templates/snippets/fav_button.html b/bookwyrm/templates/snippets/fav_button.html index 5d74f655..cd22822a 100644 --- a/bookwyrm/templates/snippets/fav_button.html +++ b/bookwyrm/templates/snippets/fav_button.html @@ -1,20 +1,19 @@ {% load bookwyrm_tags %} {% load i18n %} {% with status.id|uuid as uuid %} -
    + {% csrf_token %} - -
    + {% csrf_token %} - {% endwith %} diff --git a/bookwyrm/templates/snippets/filters_panel/filters_panel.html b/bookwyrm/templates/snippets/filters_panel/filters_panel.html index 8c178860..d9cf2852 100644 --- a/bookwyrm/templates/snippets/filters_panel/filters_panel.html +++ b/bookwyrm/templates/snippets/filters_panel/filters_panel.html @@ -11,7 +11,7 @@ - + {% if sort %} {% endif %} diff --git a/bookwyrm/templates/snippets/follow_button.html b/bookwyrm/templates/snippets/follow_button.html index 962d4d34..495bc3e9 100644 --- a/bookwyrm/templates/snippets/follow_button.html +++ b/bookwyrm/templates/snippets/follow_button.html @@ -6,12 +6,12 @@
    - + {% csrf_token %} - + {% csrf_token %} {% if user.manually_approves_followers and request.user not in user.followers.all %} diff --git a/bookwyrm/templates/snippets/generated_status/review_pure_name.html b/bookwyrm/templates/snippets/generated_status/review_pure_name.html index 90a6936e..25960191 100644 --- a/bookwyrm/templates/snippets/generated_status/review_pure_name.html +++ b/bookwyrm/templates/snippets/generated_status/review_pure_name.html @@ -1,10 +1,10 @@ {% load i18n %} {% if rating %} -{% blocktrans with book_title=book.title display_rating=rating|floatformat:"0" review_title=name count counter=rating %}Review of "{{ book_title }}" ({{ display_rating }} star): {{ review_title }}{% plural %}Review of "{{ book_title }}" ({{ display_rating }} stars): {{ review_title }}{% endblocktrans %} +{% blocktrans with book_title=book.title|safe display_rating=rating|floatformat:"0" review_title=name|safe count counter=rating %}Review of "{{ book_title }}" ({{ display_rating }} star): {{ review_title }}{% plural %}Review of "{{ book_title }}" ({{ display_rating }} stars): {{ review_title }}{% endblocktrans %} {% else %} -{% blocktrans with book_title=book.title review_title=name %}Review of "{{ book_title }}": {{ review_title }}{% endblocktrans %} +{% blocktrans with book_title=book.title|safe review_title=name|safe %}Review of "{{ book_title }}": {{ review_title }}{% endblocktrans %} {% endif %} diff --git a/bookwyrm/templates/snippets/rate_action.html b/bookwyrm/templates/snippets/rate_action.html index 9fee692d..711c3b3e 100644 --- a/bookwyrm/templates/snippets/rate_action.html +++ b/bookwyrm/templates/snippets/rate_action.html @@ -11,7 +11,7 @@ {% include 'snippets/form_rate_stars.html' with book=book classes='mb-1 has-text-warning-dark' default_rating=book|user_rating:request.user %} - - + +{% include 'snippets/pagination.html' with page=followers path=request.path %} {% endblock %} diff --git a/bookwyrm/templates/user/following.html b/bookwyrm/templates/user/following.html index 5904c1bb..38c01ad2 100644 --- a/bookwyrm/templates/user/following.html +++ b/bookwyrm/templates/user/following.html @@ -29,4 +29,6 @@
    {% blocktrans with username=user|username %}{{ username }} isn't following any users{% endblocktrans %}
    {% endif %}
    + +{% include 'snippets/pagination.html' with page=following path=request.path %} {% endblock %} diff --git a/bookwyrm/templates/user/lists.html b/bookwyrm/templates/user/lists.html index 85c7cc8c..b2fd9eef 100644 --- a/bookwyrm/templates/user/lists.html +++ b/bookwyrm/templates/user/lists.html @@ -24,7 +24,7 @@ {% block panel %}
    - +

    {% trans "Create list" %}

    diff --git a/bookwyrm/templates/user/shelf.html b/bookwyrm/templates/user/shelf.html index 0732327b..947e9f70 100644 --- a/bookwyrm/templates/user/shelf.html +++ b/bookwyrm/templates/user/shelf.html @@ -68,63 +68,66 @@
    {% if books|length > 0 %} -
    -
    - - - - - - - - - {% if ratings %}{% endif %} - {% if shelf.user == request.user %} - - {% endif %} - - {% for book in books %} - - - - - - {% latest_read_through book user as read_through %} - - - {% if ratings %} - - {% endif %} - {% if shelf.user == request.user %} - - {% endif %} - - {% endfor %} +
    {% trans "Cover" %}{% trans "Title" %}{% trans "Author" %}{% trans "Shelved" %}{% trans "Started" %}{% trans "Finished" %}{% trans "Rating" %}
    - {% include 'snippets/book_cover.html' with book=book size="small" %} - - {{ book.title }} - - {% include 'snippets/authors.html' %} - - {{ book.created_date | naturalday }} - - {{ read_through.start_date | naturalday |default_if_none:""}} - - {{ read_through.finish_date | naturalday |default_if_none:""}} - - {% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %} - - {% with right=True %} - {% if not shelf.id %} - {% active_shelf book as current %} - {% include 'snippets/shelf_selector.html' with current=current.shelf class="is-small" %} - {% else %} - {% include 'snippets/shelf_selector.html' with current=shelf class="is-small" %} - {% endif %} - {% endwith %} -
    + + + + + + + + + {% if ratings %}{% endif %} + {% if shelf.user == request.user %} + + {% endif %} + + + + {% for book in books %} + {% spaceless %} + + + + + + {% latest_read_through book user as read_through %} + + + {% if ratings %} + + {% endif %} + {% if shelf.user == request.user %} + + {% endif %} + + {% endspaceless %} + {% endfor %} +
    {% trans "Cover" %}{% trans "Title" %}{% trans "Author" %}{% trans "Shelved" %}{% trans "Started" %}{% trans "Finished" %}{% trans "Rating" %}
    + {% include 'snippets/book_cover.html' with book=book size="small" %} + + {{ book.title }} + + {% include 'snippets/authors.html' %} + + {{ book.created_date | naturalday }} + + {{ read_through.start_date | naturalday |default_if_none:""}} + + {{ read_through.finish_date | naturalday |default_if_none:""}} + + {% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %} + + {% with right=True %} + {% if not shelf.id %} + {% active_shelf book as current %} + {% include 'snippets/shelf_selector.html' with current=current.shelf class="is-small" %} + {% else %} + {% include 'snippets/shelf_selector.html' with current=shelf class="is-small" %} + {% endif %} + {% endwith %} +
    -
    {% else %}

    {% trans "This shelf is empty." %}

    {% if shelf.id and shelf.editable %} diff --git a/bookwyrm/templates/user_admin/server_filter.html b/bookwyrm/templates/user_admin/server_filter.html new file mode 100644 index 00000000..fd8760cc --- /dev/null +++ b/bookwyrm/templates/user_admin/server_filter.html @@ -0,0 +1,7 @@ +{% extends 'snippets/filters_panel/filter_field.html' %} +{% load i18n %} + +{% block filter %} + + +{% endblock %} diff --git a/bookwyrm/templates/user_admin/user.html b/bookwyrm/templates/user_admin/user.html new file mode 100644 index 00000000..46390650 --- /dev/null +++ b/bookwyrm/templates/user_admin/user.html @@ -0,0 +1,19 @@ +{% extends 'settings/admin_layout.html' %} +{% load i18n %} +{% load bookwyrm_tags %} +{% load humanize %} + +{% block title %}{{ user.username }}{% endblock %} +{% block header %}{{ user.username }}{% endblock %} + +{% block panel %} + + +{% include 'user_admin/user_info.html' with user=user %} + +{% include 'user_admin/user_moderation_actions.html' with user=user %} + +{% endblock %} + diff --git a/bookwyrm/templates/settings/user_admin.html b/bookwyrm/templates/user_admin/user_admin.html similarity index 93% rename from bookwyrm/templates/settings/user_admin.html rename to bookwyrm/templates/user_admin/user_admin.html index bb0534e0..2ab526a9 100644 --- a/bookwyrm/templates/settings/user_admin.html +++ b/bookwyrm/templates/user_admin/user_admin.html @@ -13,6 +13,8 @@ {% block panel %} +{% include 'user_admin/user_admin_filters.html' %} + {% url 'settings-users' as url %} @@ -39,7 +41,7 @@ {% for user in users %} - + diff --git a/bookwyrm/templates/user_admin/user_admin_filters.html b/bookwyrm/templates/user_admin/user_admin_filters.html new file mode 100644 index 00000000..57e017e5 --- /dev/null +++ b/bookwyrm/templates/user_admin/user_admin_filters.html @@ -0,0 +1,6 @@ +{% extends 'snippets/filters_panel/filters_panel.html' %} + +{% block filter_fields %} +{% include 'user_admin/server_filter.html' %} +{% include 'user_admin/username_filter.html' %} +{% endblock %} diff --git a/bookwyrm/templates/user_admin/user_info.html b/bookwyrm/templates/user_admin/user_info.html new file mode 100644 index 00000000..e5f5d580 --- /dev/null +++ b/bookwyrm/templates/user_admin/user_info.html @@ -0,0 +1,56 @@ +{% load i18n %} +{% load bookwyrm_tags %} +
    +
    +

    {% trans "User details" %}

    +
    + {% include 'user/user_preview.html' with user=user %} + {% if user.summary %} +
    + {{ user.summary | to_markdown | safe }} +
    + {% endif %} + +

    {% trans "View user profile" %}

    +
    +
    + {% if not user.local %} + {% with server=user.federated_server %} +
    +

    {% trans "Instance details" %}

    +
    + {% if server %} +
    {{ server.server_name }}
    +
    +
    +
    {% trans "Software:" %}
    +
    {{ server.application_type }}
    +
    +
    +
    {% trans "Version:" %}
    +
    {{ server.application_version }}
    +
    +
    +
    {% trans "Status:" %}
    +
    {{ server.status }}
    +
    +
    + {% if server.notes %} +
    {% trans "Notes" %}
    +
    + {{ server.notes }} +
    + {% endif %} + +

    + {% trans "View instance" %} +

    + {% else %} + {% trans "Not set" %} + {% endif %} +
    +
    + {% endwith %} + {% endif %} +
    + diff --git a/bookwyrm/templates/user_admin/user_moderation_actions.html b/bookwyrm/templates/user_admin/user_moderation_actions.html new file mode 100644 index 00000000..816e787a --- /dev/null +++ b/bookwyrm/templates/user_admin/user_moderation_actions.html @@ -0,0 +1,42 @@ +{% load i18n %} +
    +

    {% trans "Actions" %}

    +
    +

    + {% trans "Send direct message" %} +

    + + {% csrf_token %} + {% if user.is_active %} + + {% else %} + + {% endif %} + +
    + {% if user.local %} +
    +
    + {% csrf_token %} + + {% if group_form.non_field_errors %} + {{ group_form.non_field_errors }} + {% endif %} + {% with group=user.groups.first %} +
    + +
    + {% for error in group_form.groups.errors %} +

    {{ error | escape }}

    + {% endfor %} + {% endwith %} + + +
    + {% endif %} +
    diff --git a/bookwyrm/templates/user_admin/username_filter.html b/bookwyrm/templates/user_admin/username_filter.html new file mode 100644 index 00000000..d7da033a --- /dev/null +++ b/bookwyrm/templates/user_admin/username_filter.html @@ -0,0 +1,8 @@ +{% extends 'snippets/filters_panel/filter_field.html' %} +{% load i18n %} + +{% block filter %} + + +{% endblock %} + diff --git a/bookwyrm/templatetags/bookwyrm_tags.py b/bookwyrm/templatetags/bookwyrm_tags.py index 775c6190..2ed0cbc2 100644 --- a/bookwyrm/templatetags/bookwyrm_tags.py +++ b/bookwyrm/templatetags/bookwyrm_tags.py @@ -1,11 +1,8 @@ """ template filters """ from uuid import uuid4 -from datetime import datetime -from dateutil.relativedelta import relativedelta -from django import template +from django import template, utils from django.db.models import Avg -from django.utils import timezone from bookwyrm import models, views from bookwyrm.views.status import to_markdown @@ -16,13 +13,13 @@ register = template.Library() @register.filter(name="dict_key") def dict_key(d, k): - """ Returns the given key from a dictionary. """ + """Returns the given key from a dictionary.""" return d.get(k) or 0 @register.filter(name="rating") def get_rating(book, user): - """ get the overall rating of a book """ + """get the overall rating of a book""" queryset = views.helpers.privacy_filter( user, models.Review.objects.filter(book=book) ) @@ -31,7 +28,7 @@ def get_rating(book, user): @register.filter(name="user_rating") def get_user_rating(book, user): - """ get a user's rating of a book """ + """get a user's rating of a book""" rating = ( models.Review.objects.filter( user=user, @@ -48,33 +45,29 @@ def get_user_rating(book, user): @register.filter(name="username") def get_user_identifier(user): - """ use localname for local users, username for remote """ + """use localname for local users, username for remote""" return user.localname if user.localname else user.username @register.filter(name="notification_count") def get_notification_count(user): - """ how many UNREAD notifications are there """ + """how many UNREAD notifications are there""" return user.notification_set.filter(read=False).count() @register.filter(name="replies") def get_replies(status): - """ get all direct replies to a status """ + """get all direct replies to a status""" # TODO: this limit could cause problems - return ( - models.Status.objects.filter( - reply_parent=status, - deleted=False, - ) - .select_subclasses() - .all()[:10] - ) + return models.Status.objects.filter( + reply_parent=status, + deleted=False, + ).select_subclasses()[:10] @register.filter(name="parent") def get_parent(status): - """ get the reply parent for a status """ + """get the reply parent for a status""" return ( models.Status.objects.filter(id=status.reply_parent_id) .select_subclasses() @@ -84,7 +77,7 @@ def get_parent(status): @register.filter(name="liked") def get_user_liked(user, status): - """ did the given user fav a status? """ + """did the given user fav a status?""" try: models.Favorite.objects.get(user=user, status=status) return True @@ -94,13 +87,13 @@ def get_user_liked(user, status): @register.filter(name="boosted") def get_user_boosted(user, status): - """ did the given user fav a status? """ + """did the given user fav a status?""" return user.id in status.boosters.all().values_list("user", flat=True) @register.filter(name="follow_request_exists") def follow_request_exists(user, requester): - """ see if there is a pending follow request for a user """ + """see if there is a pending follow request for a user""" try: models.UserFollowRequest.objects.filter( user_subject=requester, @@ -113,7 +106,7 @@ def follow_request_exists(user, requester): @register.filter(name="boosted_status") def get_boosted(boost): - """ load a boosted status. have to do this or it wont get foregin keys """ + """load a boosted status. have to do this or it wont get foregin keys""" return ( models.Status.objects.select_subclasses() .filter(id=boost.boosted_status.id) @@ -123,41 +116,19 @@ def get_boosted(boost): @register.filter(name="book_description") def get_book_description(book): - """ use the work's text if the book doesn't have it """ + """use the work's text if the book doesn't have it""" return book.description or book.parent_work.description @register.filter(name="uuid") def get_uuid(identifier): - """ for avoiding clashing ids when there are many forms """ + """for avoiding clashing ids when there are many forms""" return "%s%s" % (identifier, uuid4()) -@register.filter(name="post_date") -def time_since(date): - """ concise time ago function """ - if not isinstance(date, datetime): - return "" - now = timezone.now() - - if date < (now - relativedelta(weeks=1)): - formatter = "%b %-d" - if date.year != now.year: - formatter += " %Y" - return date.strftime(formatter) - delta = relativedelta(now, date) - if delta.days: - return "%dd" % delta.days - if delta.hours: - return "%dh" % delta.hours - if delta.minutes: - return "%dm" % delta.minutes - return "%ds" % delta.seconds - - @register.filter(name="to_markdown") def get_markdown(content): - """ convert markdown to html """ + """convert markdown to html""" if content: return to_markdown(content) return None @@ -165,7 +136,7 @@ def get_markdown(content): @register.filter(name="mentions") def get_mentions(status, user): - """ people to @ in a reply: the parent and all mentions """ + """people to @ in a reply: the parent and all mentions""" mentions = set([status.user] + list(status.mention_users.all())) return ( " ".join("@" + get_user_identifier(m) for m in mentions if not m == user) + " " @@ -174,7 +145,7 @@ def get_mentions(status, user): @register.filter(name="status_preview_name") def get_status_preview_name(obj): - """ text snippet with book context for a status """ + """text snippet with book context for a status""" name = obj.__class__.__name__.lower() if name == "review": return "%s of %s" % (name, obj.book.title) @@ -187,7 +158,7 @@ def get_status_preview_name(obj): @register.filter(name="next_shelf") def get_next_shelf(current_shelf): - """ shelf you'd use to update reading progress """ + """shelf you'd use to update reading progress""" if current_shelf == "to-read": return "reading" if current_shelf == "reading": @@ -197,9 +168,20 @@ def get_next_shelf(current_shelf): return "to-read" +@register.filter(name="title") +def get_title(book): + """display the subtitle if the title is short""" + if not book: + return "" + title = book.title + if len(title) < 6 and book.subtitle: + title = "{:s}: {:s}".format(title, book.subtitle) + return title + + @register.simple_tag(takes_context=False) def related_status(notification): - """ for notifications """ + """for notifications""" if not notification.related_status: return None if hasattr(notification.related_status, "quotation"): @@ -213,7 +195,7 @@ def related_status(notification): @register.simple_tag(takes_context=True) def active_shelf(context, book): - """ check what shelf a user has a book on, if any """ + """check what shelf a user has a book on, if any""" shelf = models.ShelfBook.objects.filter( shelf__user=context["request"].user, book__in=book.parent_work.editions.all() ).first() @@ -222,7 +204,7 @@ def active_shelf(context, book): @register.simple_tag(takes_context=False) def latest_read_through(book, user): - """ the most recent read activity """ + """the most recent read activity""" return ( models.ReadThrough.objects.filter(user=user, book=book) .order_by("-start_date") @@ -232,7 +214,7 @@ def latest_read_through(book, user): @register.simple_tag(takes_context=False) def active_read_through(book, user): - """ the most recent read activity """ + """the most recent read activity""" return ( models.ReadThrough.objects.filter( user=user, book=book, finish_date__isnull=True @@ -244,5 +226,12 @@ def active_read_through(book, user): @register.simple_tag(takes_context=False) def comparison_bool(str1, str2): - """ idk why I need to write a tag for this, it reutrns a bool """ + """idk why I need to write a tag for this, it reutrns a bool""" return str1 == str2 + + +@register.simple_tag(takes_context=False) +def get_lang(): + """get current language, strip to the first two letters""" + language = utils.translation.get_language() + return language[0 : language.find("-")] diff --git a/bookwyrm/tests/activitypub/test_base_activity.py b/bookwyrm/tests/activitypub/test_base_activity.py index 8ec0b703..77844a22 100644 --- a/bookwyrm/tests/activitypub/test_base_activity.py +++ b/bookwyrm/tests/activitypub/test_base_activity.py @@ -21,10 +21,10 @@ from bookwyrm import models @patch("bookwyrm.activitystreams.ActivityStream.add_status") class BaseActivity(TestCase): - """ the super class for model-linked activitypub dataclasses """ + """the super class for model-linked activitypub dataclasses""" def setUp(self): - """ we're probably going to re-use this so why copy/paste """ + """we're probably going to re-use this so why copy/paste""" self.user = models.User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" ) @@ -45,28 +45,28 @@ class BaseActivity(TestCase): self.image_data = output.getvalue() def test_init(self, _): - """ simple successfuly init """ + """simple successfuly init""" instance = ActivityObject(id="a", type="b") self.assertTrue(hasattr(instance, "id")) self.assertTrue(hasattr(instance, "type")) def test_init_missing(self, _): - """ init with missing required params """ + """init with missing required params""" with self.assertRaises(ActivitySerializerError): ActivityObject() def test_init_extra_fields(self, _): - """ init ignoring additional fields """ + """init ignoring additional fields""" instance = ActivityObject(id="a", type="b", fish="c") self.assertTrue(hasattr(instance, "id")) self.assertTrue(hasattr(instance, "type")) def test_init_default_field(self, _): - """ replace an existing required field with a default field """ + """replace an existing required field with a default field""" @dataclass(init=False) class TestClass(ActivityObject): - """ test class with default field """ + """test class with default field""" type: str = "TestObject" @@ -75,7 +75,7 @@ class BaseActivity(TestCase): self.assertEqual(instance.type, "TestObject") def test_serialize(self, _): - """ simple function for converting dataclass to dict """ + """simple function for converting dataclass to dict""" instance = ActivityObject(id="a", type="b") serialized = instance.serialize() self.assertIsInstance(serialized, dict) @@ -84,7 +84,7 @@ class BaseActivity(TestCase): @responses.activate def test_resolve_remote_id(self, _): - """ look up or load remote data """ + """look up or load remote data""" # existing item result = resolve_remote_id("http://example.com/a/b", model=models.User) self.assertEqual(result, self.user) @@ -106,14 +106,14 @@ class BaseActivity(TestCase): self.assertEqual(result.name, "MOUSE?? MOUSE!!") def test_to_model_invalid_model(self, _): - """ catch mismatch between activity type and model type """ + """catch mismatch between activity type and model type""" instance = ActivityObject(id="a", type="b") with self.assertRaises(ActivitySerializerError): instance.to_model(model=models.User) @responses.activate def test_to_model_image(self, _): - """ update an image field """ + """update an image field""" activity = activitypub.Person( id=self.user.remote_id, name="New Name", @@ -146,7 +146,7 @@ class BaseActivity(TestCase): self.assertEqual(self.user.key_pair.public_key, "hi") def test_to_model_many_to_many(self, _): - """ annoying that these all need special handling """ + """annoying that these all need special handling""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create( content="test status", @@ -216,7 +216,7 @@ class BaseActivity(TestCase): @responses.activate def test_set_related_field(self, _): - """ celery task to add back-references to created objects """ + """celery task to add back-references to created objects""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create( content="test status", diff --git a/bookwyrm/tests/activitypub/test_quotation.py b/bookwyrm/tests/activitypub/test_quotation.py index 1f429dd2..c90348bc 100644 --- a/bookwyrm/tests/activitypub/test_quotation.py +++ b/bookwyrm/tests/activitypub/test_quotation.py @@ -8,10 +8,10 @@ from bookwyrm import activitypub, models class Quotation(TestCase): - """ we have hecka ways to create statuses """ + """we have hecka ways to create statuses""" def setUp(self): - """ model objects we'll need """ + """model objects we'll need""" with patch("bookwyrm.models.user.set_remote_server.delay"): self.user = models.User.objects.create_user( "mouse", @@ -30,7 +30,7 @@ class Quotation(TestCase): self.status_data = json.loads(datafile.read_bytes()) def test_quotation_activity(self): - """ create a Quoteation ap object from json """ + """create a Quoteation ap object from json""" quotation = activitypub.Quotation(**self.status_data) self.assertEqual(quotation.type, "Quotation") @@ -41,7 +41,7 @@ class Quotation(TestCase): self.assertEqual(quotation.published, "2020-05-10T02:38:31.150343+00:00") def test_activity_to_model(self): - """ create a model instance from an activity object """ + """create a model instance from an activity object""" activity = activitypub.Quotation(**self.status_data) quotation = activity.to_model(model=models.Quotation) diff --git a/bookwyrm/tests/connectors/test_abstract_connector.py b/bookwyrm/tests/connectors/test_abstract_connector.py index 97190c16..4497b4e5 100644 --- a/bookwyrm/tests/connectors/test_abstract_connector.py +++ b/bookwyrm/tests/connectors/test_abstract_connector.py @@ -10,10 +10,10 @@ from bookwyrm.settings import DOMAIN class AbstractConnector(TestCase): - """ generic code for connecting to outside data sources """ + """generic code for connecting to outside data sources""" def setUp(self): - """ we need an example connector """ + """we need an example connector""" self.connector_info = models.Connector.objects.create( identifier="example.com", connector_file="openlibrary", @@ -38,7 +38,7 @@ class AbstractConnector(TestCase): self.edition_data = edition_data class TestConnector(abstract_connector.AbstractConnector): - """ nothing added here """ + """nothing added here""" def format_search_result(self, search_result): return search_result @@ -81,18 +81,18 @@ class AbstractConnector(TestCase): ) def test_abstract_connector_init(self): - """ barebones connector for search with defaults """ + """barebones connector for search with defaults""" self.assertIsInstance(self.connector.book_mappings, list) def test_is_available(self): - """ this isn't used.... """ + """this isn't used....""" self.assertTrue(self.connector.is_available()) self.connector.max_query_count = 1 self.connector.connector.query_count = 2 self.assertFalse(self.connector.is_available()) def test_get_or_create_book_existing(self): - """ find an existing book by remote/origin id """ + """find an existing book by remote/origin id""" self.assertEqual(models.Book.objects.count(), 1) self.assertEqual( self.book.remote_id, "https://%s/book/%d" % (DOMAIN, self.book.id) @@ -113,7 +113,7 @@ class AbstractConnector(TestCase): @responses.activate def test_get_or_create_book_deduped(self): - """ load remote data and deduplicate """ + """load remote data and deduplicate""" responses.add( responses.GET, "https://example.com/book/abcd", json=self.edition_data ) @@ -125,7 +125,7 @@ class AbstractConnector(TestCase): @responses.activate def test_get_or_create_author(self): - """ load an author """ + """load an author""" self.connector.author_mappings = [ Mapping("id"), Mapping("name"), @@ -142,7 +142,7 @@ class AbstractConnector(TestCase): self.assertEqual(result.origin_id, "https://www.example.com/author") def test_get_or_create_author_existing(self): - """ get an existing author """ + """get an existing author""" author = models.Author.objects.create(name="Test Author") result = self.connector.get_or_create_author(author.remote_id) self.assertEqual(author, result) diff --git a/bookwyrm/tests/connectors/test_abstract_minimal_connector.py b/bookwyrm/tests/connectors/test_abstract_minimal_connector.py index fa7c85f4..bc5625c9 100644 --- a/bookwyrm/tests/connectors/test_abstract_minimal_connector.py +++ b/bookwyrm/tests/connectors/test_abstract_minimal_connector.py @@ -8,10 +8,10 @@ from bookwyrm.connectors.abstract_connector import Mapping, SearchResult class AbstractConnector(TestCase): - """ generic code for connecting to outside data sources """ + """generic code for connecting to outside data sources""" def setUp(self): - """ we need an example connector """ + """we need an example connector""" self.connector_info = models.Connector.objects.create( identifier="example.com", connector_file="openlibrary", @@ -23,7 +23,7 @@ class AbstractConnector(TestCase): ) class TestConnector(abstract_connector.AbstractMinimalConnector): - """ nothing added here """ + """nothing added here""" def format_search_result(self, search_result): return search_result @@ -43,7 +43,7 @@ class AbstractConnector(TestCase): self.test_connector = TestConnector("example.com") def test_abstract_minimal_connector_init(self): - """ barebones connector for search with defaults """ + """barebones connector for search with defaults""" connector = self.test_connector self.assertEqual(connector.connector, self.connector_info) self.assertEqual(connector.base_url, "https://example.com") @@ -58,7 +58,7 @@ class AbstractConnector(TestCase): @responses.activate def test_search(self): - """ makes an http request to the outside service """ + """makes an http request to the outside service""" responses.add( responses.GET, "https://example.com/search?q=a%20book%20title", @@ -73,7 +73,7 @@ class AbstractConnector(TestCase): @responses.activate def test_search_min_confidence(self): - """ makes an http request to the outside service """ + """makes an http request to the outside service""" responses.add( responses.GET, "https://example.com/search?q=a%20book%20title&min_confidence=1", @@ -85,7 +85,7 @@ class AbstractConnector(TestCase): @responses.activate def test_isbn_search(self): - """ makes an http request to the outside service """ + """makes an http request to the outside service""" responses.add( responses.GET, "https://example.com/isbn?q=123456", @@ -96,7 +96,7 @@ class AbstractConnector(TestCase): self.assertEqual(len(results), 10) def test_search_result(self): - """ a class that stores info about a search result """ + """a class that stores info about a search result""" result = SearchResult( title="Title", key="https://example.com/book/1", @@ -109,21 +109,21 @@ class AbstractConnector(TestCase): self.assertEqual(result.title, "Title") def test_create_mapping(self): - """ maps remote fields for book data to bookwyrm activitypub fields """ + """maps remote fields for book data to bookwyrm activitypub fields""" mapping = Mapping("isbn") self.assertEqual(mapping.local_field, "isbn") self.assertEqual(mapping.remote_field, "isbn") self.assertEqual(mapping.formatter("bb"), "bb") def test_create_mapping_with_remote(self): - """ the remote field is different than the local field """ + """the remote field is different than the local field""" mapping = Mapping("isbn", remote_field="isbn13") self.assertEqual(mapping.local_field, "isbn") self.assertEqual(mapping.remote_field, "isbn13") self.assertEqual(mapping.formatter("bb"), "bb") def test_create_mapping_with_formatter(self): - """ a function is provided to modify the data """ + """a function is provided to modify the data""" formatter = lambda x: "aa" + x mapping = Mapping("isbn", formatter=formatter) self.assertEqual(mapping.local_field, "isbn") diff --git a/bookwyrm/tests/connectors/test_bookwyrm_connector.py b/bookwyrm/tests/connectors/test_bookwyrm_connector.py index 386c1350..46ea54a9 100644 --- a/bookwyrm/tests/connectors/test_bookwyrm_connector.py +++ b/bookwyrm/tests/connectors/test_bookwyrm_connector.py @@ -9,10 +9,10 @@ from bookwyrm.connectors.abstract_connector import SearchResult class BookWyrmConnector(TestCase): - """ this connector doesn't do much, just search """ + """this connector doesn't do much, just search""" def setUp(self): - """ create the connector """ + """create the connector""" models.Connector.objects.create( identifier="example.com", connector_file="bookwyrm_connector", @@ -24,14 +24,14 @@ class BookWyrmConnector(TestCase): self.connector = Connector("example.com") def test_get_or_create_book_existing(self): - """ load book activity """ + """load book activity""" work = models.Work.objects.create(title="Test Work") book = models.Edition.objects.create(title="Test Edition", parent_work=work) result = self.connector.get_or_create_book(book.remote_id) self.assertEqual(book, result) def test_format_search_result(self): - """ create a SearchResult object from search response json """ + """create a SearchResult object from search response json""" datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_search.json") search_data = json.loads(datafile.read_bytes()) results = self.connector.parse_search_data(search_data) @@ -46,7 +46,7 @@ class BookWyrmConnector(TestCase): self.assertEqual(result.connector, self.connector) def test_format_isbn_search_result(self): - """ just gotta attach the connector """ + """just gotta attach the connector""" datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_search.json") search_data = json.loads(datafile.read_bytes()) results = self.connector.parse_isbn_search_data(search_data) diff --git a/bookwyrm/tests/connectors/test_connector_manager.py b/bookwyrm/tests/connectors/test_connector_manager.py index 52589323..feded616 100644 --- a/bookwyrm/tests/connectors/test_connector_manager.py +++ b/bookwyrm/tests/connectors/test_connector_manager.py @@ -8,10 +8,10 @@ from bookwyrm.connectors.self_connector import Connector as SelfConnector class ConnectorManager(TestCase): - """ interface between the app and various connectors """ + """interface between the app and various connectors""" def setUp(self): - """ we'll need some books and a connector info entry """ + """we'll need some books and a connector info entry""" self.work = models.Work.objects.create(title="Example Work") self.edition = models.Edition.objects.create( @@ -32,7 +32,7 @@ class ConnectorManager(TestCase): ) def test_get_or_create_connector(self): - """ loads a connector if the data source is known or creates one """ + """loads a connector if the data source is known or creates one""" remote_id = "https://example.com/object/1" connector = connector_manager.get_or_create_connector(remote_id) self.assertIsInstance(connector, BookWyrmConnector) @@ -43,7 +43,7 @@ class ConnectorManager(TestCase): self.assertEqual(connector.identifier, same_connector.identifier) def test_get_connectors(self): - """ load all connectors """ + """load all connectors""" remote_id = "https://example.com/object/1" connector_manager.get_or_create_connector(remote_id) connectors = list(connector_manager.get_connectors()) @@ -52,7 +52,7 @@ class ConnectorManager(TestCase): self.assertIsInstance(connectors[1], BookWyrmConnector) def test_search(self): - """ search all connectors """ + """search all connectors""" results = connector_manager.search("Example") self.assertEqual(len(results), 1) self.assertIsInstance(results[0]["connector"], SelfConnector) @@ -60,7 +60,7 @@ class ConnectorManager(TestCase): self.assertEqual(results[0]["results"][0].title, "Example Edition") def test_search_isbn(self): - """ special handling if a query resembles an isbn """ + """special handling if a query resembles an isbn""" results = connector_manager.search("0000000000") self.assertEqual(len(results), 1) self.assertIsInstance(results[0]["connector"], SelfConnector) @@ -68,20 +68,20 @@ class ConnectorManager(TestCase): self.assertEqual(results[0]["results"][0].title, "Example Edition") def test_local_search(self): - """ search only the local database """ + """search only the local database""" results = connector_manager.local_search("Example") self.assertEqual(len(results), 1) self.assertEqual(results[0].title, "Example Edition") def test_first_search_result(self): - """ only get one search result """ + """only get one search result""" result = connector_manager.first_search_result("Example") self.assertEqual(result.title, "Example Edition") no_result = connector_manager.first_search_result("dkjfhg") self.assertIsNone(no_result) def test_load_connector(self): - """ load a connector object from the database entry """ + """load a connector object from the database entry""" connector = connector_manager.load_connector(self.connector) self.assertIsInstance(connector, SelfConnector) self.assertEqual(connector.identifier, "test_connector") diff --git a/bookwyrm/tests/connectors/test_openlibrary_connector.py b/bookwyrm/tests/connectors/test_openlibrary_connector.py index 3cff4fb0..699b26ed 100644 --- a/bookwyrm/tests/connectors/test_openlibrary_connector.py +++ b/bookwyrm/tests/connectors/test_openlibrary_connector.py @@ -16,10 +16,10 @@ from bookwyrm.connectors.connector_manager import ConnectorException class Openlibrary(TestCase): - """ test loading data from openlibrary.org """ + """test loading data from openlibrary.org""" def setUp(self): - """ creates the connector we'll use """ + """creates the connector we'll use""" models.Connector.objects.create( identifier="openlibrary.org", name="OpenLibrary", @@ -42,7 +42,7 @@ class Openlibrary(TestCase): self.edition_list_data = json.loads(edition_list_file.read_bytes()) def test_get_remote_id_from_data(self): - """ format the remote id from the data """ + """format the remote id from the data""" data = {"key": "/work/OL1234W"} result = self.connector.get_remote_id_from_data(data) self.assertEqual(result, "https://openlibrary.org/work/OL1234W") @@ -51,13 +51,13 @@ class Openlibrary(TestCase): self.connector.get_remote_id_from_data({}) def test_is_work_data(self): - """ detect if the loaded json is a work """ + """detect if the loaded json is a work""" self.assertEqual(self.connector.is_work_data(self.work_data), True) self.assertEqual(self.connector.is_work_data(self.edition_data), False) @responses.activate def test_get_edition_from_work_data(self): - """ loads a list of editions """ + """loads a list of editions""" data = {"key": "/work/OL1234W"} responses.add( responses.GET, @@ -74,7 +74,7 @@ class Openlibrary(TestCase): @responses.activate def test_get_work_from_edition_data(self): - """ loads a list of editions """ + """loads a list of editions""" data = {"works": [{"key": "/work/OL1234W"}]} responses.add( responses.GET, @@ -87,7 +87,7 @@ class Openlibrary(TestCase): @responses.activate def test_get_authors_from_data(self): - """ find authors in data """ + """find authors in data""" responses.add( responses.GET, "https://openlibrary.org/authors/OL382982A", @@ -112,13 +112,13 @@ class Openlibrary(TestCase): self.assertEqual(result.openlibrary_key, "OL453734A") def test_get_cover_url(self): - """ formats a url that should contain the cover image """ + """formats a url that should contain the cover image""" blob = ["image"] result = self.connector.get_cover_url(blob) self.assertEqual(result, "https://covers.openlibrary.org/b/id/image-L.jpg") def test_parse_search_result(self): - """ extract the results from the search json response """ + """extract the results from the search json response""" datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_search.json") search_data = json.loads(datafile.read_bytes()) result = self.connector.parse_search_data(search_data) @@ -126,7 +126,7 @@ class Openlibrary(TestCase): self.assertEqual(len(result), 2) def test_format_search_result(self): - """ translate json from openlibrary into SearchResult """ + """translate json from openlibrary into SearchResult""" datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_search.json") search_data = json.loads(datafile.read_bytes()) results = self.connector.parse_search_data(search_data) @@ -141,7 +141,7 @@ class Openlibrary(TestCase): self.assertEqual(result.connector, self.connector) def test_parse_isbn_search_result(self): - """ extract the results from the search json response """ + """extract the results from the search json response""" datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_isbn_search.json") search_data = json.loads(datafile.read_bytes()) result = self.connector.parse_isbn_search_data(search_data) @@ -149,7 +149,7 @@ class Openlibrary(TestCase): self.assertEqual(len(result), 1) def test_format_isbn_search_result(self): - """ translate json from openlibrary into SearchResult """ + """translate json from openlibrary into SearchResult""" datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_isbn_search.json") search_data = json.loads(datafile.read_bytes()) results = self.connector.parse_isbn_search_data(search_data) @@ -165,7 +165,7 @@ class Openlibrary(TestCase): @responses.activate def test_load_edition_data(self): - """ format url from key and make request """ + """format url from key and make request""" key = "OL1234W" responses.add( responses.GET, @@ -177,7 +177,7 @@ class Openlibrary(TestCase): @responses.activate def test_expand_book_data(self): - """ given a book, get more editions """ + """given a book, get more editions""" work = models.Work.objects.create(title="Test Work", openlibrary_key="OL1234W") edition = models.Edition.objects.create(title="Test Edition", parent_work=work) @@ -194,29 +194,29 @@ class Openlibrary(TestCase): self.connector.expand_book_data(work) def test_get_description(self): - """ should do some cleanup on the description data """ + """should do some cleanup on the description data""" description = get_description(self.work_data["description"]) expected = "First in the Old Kingdom/Abhorsen series." self.assertEqual(description, expected) def test_get_openlibrary_key(self): - """ extracts the uuid """ + """extracts the uuid""" key = get_openlibrary_key("/books/OL27320736M") self.assertEqual(key, "OL27320736M") def test_get_languages(self): - """ looks up languages from a list """ + """looks up languages from a list""" languages = get_languages(self.edition_data["languages"]) self.assertEqual(languages, ["English"]) def test_pick_default_edition(self): - """ detect if the loaded json is an edition """ + """detect if the loaded json is an edition""" edition = pick_default_edition(self.edition_list_data["entries"]) self.assertEqual(edition["key"], "/books/OL9788823M") @responses.activate def test_create_edition_from_data(self): - """ okay but can it actually create an edition with proper metadata """ + """okay but can it actually create an edition with proper metadata""" work = models.Work.objects.create(title="Hello") responses.add( responses.GET, @@ -240,7 +240,7 @@ class Openlibrary(TestCase): self.assertEqual(result.physical_format, "Hardcover") def test_ignore_edition(self): - """ skip editions with poor metadata """ + """skip editions with poor metadata""" self.assertFalse(ignore_edition({"isbn_13": "hi"})) self.assertFalse(ignore_edition({"oclc_numbers": "hi"})) self.assertFalse(ignore_edition({"covers": "hi"})) diff --git a/bookwyrm/tests/connectors/test_self_connector.py b/bookwyrm/tests/connectors/test_self_connector.py index 9925f594..eee7d00c 100644 --- a/bookwyrm/tests/connectors/test_self_connector.py +++ b/bookwyrm/tests/connectors/test_self_connector.py @@ -9,10 +9,10 @@ from bookwyrm.settings import DOMAIN class SelfConnector(TestCase): - """ just uses local data """ + """just uses local data""" def setUp(self): - """ creating the connector """ + """creating the connector""" models.Connector.objects.create( identifier=DOMAIN, name="Local", @@ -27,7 +27,7 @@ class SelfConnector(TestCase): self.connector = Connector(DOMAIN) def test_format_search_result(self): - """ create a SearchResult """ + """create a SearchResult""" author = models.Author.objects.create(name="Anonymous") edition = models.Edition.objects.create( title="Edition of Example Work", @@ -42,7 +42,7 @@ class SelfConnector(TestCase): self.assertEqual(result.connector, self.connector) def test_search_rank(self): - """ prioritize certain results """ + """prioritize certain results""" author = models.Author.objects.create(name="Anonymous") edition = models.Edition.objects.create( title="Edition of Example Work", @@ -78,7 +78,7 @@ class SelfConnector(TestCase): self.assertEqual(results[2].title, "Edition of Example Work") def test_search_multiple_editions(self): - """ it should get rid of duplicate editions for the same work """ + """it should get rid of duplicate editions for the same work""" work = models.Work.objects.create(title="Work Title") edition_1 = models.Edition.objects.create( title="Edition 1 Title", parent_work=work diff --git a/bookwyrm/tests/data/ap_user_rat.json b/bookwyrm/tests/data/ap_user_rat.json new file mode 100644 index 00000000..0e36f1c6 --- /dev/null +++ b/bookwyrm/tests/data/ap_user_rat.json @@ -0,0 +1,39 @@ +{ + "@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" + } + ], + "id": "https://example.com/users/rat", + "type": "Person", + "preferredUsername": "rat", + "name": "RAT???", + "inbox": "https://example.com/users/rat/inbox", + "outbox": "https://example.com/users/rat/outbox", + "followers": "https://example.com/users/rat/followers", + "following": "https://example.com/users/rat/following", + "summary": "", + "publicKey": { + "id": "https://example.com/users/rat/#main-key", + "owner": "https://example.com/users/rat", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6QisDrjOQvkRo/MqNmSYPwqtt\nCxg/8rCW+9jKbFUKvqjTeKVotEE85122v/DCvobCCdfQuYIFdVMk+dB1xJ0iPGPg\nyU79QHY22NdV9mFKA2qtXVVxb5cxpA4PlwOHM6PM/k8B+H09OUrop2aPUAYwy+vg\n+MXyz8bAXrIS1kq6fQIDAQAB\n-----END PUBLIC KEY-----" + }, + "endpoints": { + "sharedInbox": "https://example.com/inbox" + }, + "bookwyrmUser": true, + "manuallyApprovesFollowers": false, + "discoverable": true, + "devices": "https://friend.camp/users/tripofmice/collections/devices", + "tag": [], + "icon": { + "type": "Image", + "mediaType": "image/png", + "url": "https://example.com/images/avatars/AL-2-crop-50.png" + } +} diff --git a/bookwyrm/tests/data/bw_edition.json b/bookwyrm/tests/data/bw_edition.json index 0cc17d29..6194e409 100644 --- a/bookwyrm/tests/data/bw_edition.json +++ b/bookwyrm/tests/data/bw_edition.json @@ -1,5 +1,6 @@ { "id": "https://bookwyrm.social/book/5989", + "lastEditedBy": "https://example.com/users/rat", "type": "Edition", "authors": [ "https://bookwyrm.social/author/417" diff --git a/bookwyrm/tests/data/goodreads-rating.csv b/bookwyrm/tests/data/goodreads-rating.csv new file mode 100644 index 00000000..fec0c77d --- /dev/null +++ b/bookwyrm/tests/data/goodreads-rating.csv @@ -0,0 +1,5 @@ +Book Id,Title,Author,Author l-f,Additional Authors,ISBN,ISBN13,My Rating,Average Rating,Publisher,Binding,Number of Pages,Year Published,Original Publication Year,Date Read,Date Added,Bookshelves,Bookshelves with positions,Exclusive Shelf,My Review,Spoiler,Private Notes,Read Count,Recommended For,Recommended By,Owned Copies,Original Purchase Date,Original Purchase Location,Condition,Condition Description,BCID +42036538,Gideon the Ninth (The Locked Tomb #1),Tamsyn Muir,"Muir, Tamsyn",,"=""1250313198""","=""9781250313195""",0,4.20,Tor,Hardcover,448,2019,2019,2020/10/25,2020/10/21,,,read,,,,1,,,0,,,,, +52691223,Subcutanean,Aaron A. Reed,"Reed, Aaron A.",,"=""""","=""""",0,4.45,,Paperback,232,2020,,2020/03/06,2020/03/05,,,read,,,,1,,,0,,,,, +28694510,Patisserie at Home,Mélanie Dupuis,"Dupuis, Mélanie",Anne Cazor,"=""0062445316""","=""9780062445315""",2,4.60,Harper Design,Hardcover,288,2016,,,2019/07/08,,,read,,,,2,,,0,,,,, + diff --git a/bookwyrm/tests/importers/test_goodreads_import.py b/bookwyrm/tests/importers/test_goodreads_import.py index 6e9caaf4..655567d0 100644 --- a/bookwyrm/tests/importers/test_goodreads_import.py +++ b/bookwyrm/tests/importers/test_goodreads_import.py @@ -14,10 +14,10 @@ from bookwyrm.settings import DOMAIN class GoodreadsImport(TestCase): - """ importing from goodreads csv """ + """importing from goodreads csv""" def setUp(self): - """ use a test csv """ + """use a test csv""" self.importer = GoodreadsImporter() datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") self.csv = open(datafile, "r", encoding=self.importer.encoding) @@ -44,7 +44,7 @@ class GoodreadsImport(TestCase): ) def test_create_job(self): - """ creates the import job entry and checks csv """ + """creates the import job entry and checks csv""" import_job = self.importer.create_job(self.user, self.csv, False, "public") self.assertEqual(import_job.user, self.user) self.assertEqual(import_job.include_reviews, False) @@ -60,7 +60,7 @@ class GoodreadsImport(TestCase): self.assertEqual(import_items[2].data["Book Id"], "28694510") def test_create_retry_job(self): - """ trying again with items that didn't import """ + """trying again with items that didn't import""" import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") import_items = models.ImportItem.objects.filter(job=import_job).all()[:2] @@ -78,7 +78,7 @@ class GoodreadsImport(TestCase): self.assertEqual(retry_items[1].data["Book Id"], "52691223") def test_start_import(self): - """ begin loading books """ + """begin loading books""" import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") MockTask = namedtuple("Task", ("id")) mock_task = MockTask(7) @@ -90,7 +90,7 @@ class GoodreadsImport(TestCase): @responses.activate def test_import_data(self): - """ resolve entry """ + """resolve entry""" import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") book = models.Edition.objects.create(title="Test Book") @@ -105,7 +105,7 @@ class GoodreadsImport(TestCase): self.assertEqual(import_item.book.id, book.id) def test_handle_imported_book(self): - """ goodreads import added a book, this adds related connections """ + """goodreads import added a book, this adds related connections""" shelf = self.user.shelf_set.filter(identifier="read").first() self.assertIsNone(shelf.books.first()) @@ -138,7 +138,7 @@ class GoodreadsImport(TestCase): self.assertEqual(readthrough.finish_date.day, 25) def test_handle_imported_book_already_shelved(self): - """ goodreads import added a book, this adds related connections """ + """goodreads import added a book, this adds related connections""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): shelf = self.user.shelf_set.filter(identifier="to-read").first() models.ShelfBook.objects.create(shelf=shelf, user=self.user, book=self.book) @@ -171,7 +171,7 @@ class GoodreadsImport(TestCase): self.assertEqual(readthrough.finish_date.day, 25) def test_handle_import_twice(self): - """ re-importing books """ + """re-importing books""" shelf = self.user.shelf_set.filter(identifier="read").first() import_job = models.ImportJob.objects.create(user=self.user) datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") @@ -206,7 +206,7 @@ class GoodreadsImport(TestCase): @patch("bookwyrm.activitystreams.ActivityStream.add_status") def test_handle_imported_book_review(self, _): - """ goodreads review import """ + """goodreads review import""" import_job = models.ImportJob.objects.create(user=self.user) datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") csv_file = open(datafile, "r") @@ -228,8 +228,34 @@ class GoodreadsImport(TestCase): self.assertEqual(review.published_date.day, 8) self.assertEqual(review.privacy, "unlisted") + @patch("bookwyrm.activitystreams.ActivityStream.add_status") + def test_handle_imported_book_rating(self, _): + """goodreads rating import""" + import_job = models.ImportJob.objects.create(user=self.user) + datafile = pathlib.Path(__file__).parent.joinpath( + "../data/goodreads-rating.csv" + ) + csv_file = open(datafile, "r") + entry = list(csv.DictReader(csv_file))[2] + entry = self.importer.parse_fields(entry) + import_item = models.ImportItem.objects.create( + job_id=import_job.id, index=0, data=entry, book=self.book + ) + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + handle_imported_book( + self.importer.service, self.user, import_item, True, "unlisted" + ) + review = models.ReviewRating.objects.get(book=self.book, user=self.user) + self.assertIsInstance(review, models.ReviewRating) + self.assertEqual(review.rating, 2) + self.assertEqual(review.published_date.year, 2019) + self.assertEqual(review.published_date.month, 7) + self.assertEqual(review.published_date.day, 8) + self.assertEqual(review.privacy, "unlisted") + def test_handle_imported_book_reviews_disabled(self): - """ goodreads review import """ + """goodreads review import""" import_job = models.ImportJob.objects.create(user=self.user) datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") csv_file = open(datafile, "r") diff --git a/bookwyrm/tests/importers/test_librarything_import.py b/bookwyrm/tests/importers/test_librarything_import.py index 5e1d778e..5ae0944c 100644 --- a/bookwyrm/tests/importers/test_librarything_import.py +++ b/bookwyrm/tests/importers/test_librarything_import.py @@ -13,10 +13,10 @@ from bookwyrm.settings import DOMAIN class LibrarythingImport(TestCase): - """ importing from librarything tsv """ + """importing from librarything tsv""" def setUp(self): - """ use a test tsv """ + """use a test tsv""" self.importer = LibrarythingImporter() datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") @@ -45,7 +45,7 @@ class LibrarythingImport(TestCase): ) def test_create_job(self): - """ creates the import job entry and checks csv """ + """creates the import job entry and checks csv""" import_job = self.importer.create_job(self.user, self.csv, False, "public") self.assertEqual(import_job.user, self.user) self.assertEqual(import_job.include_reviews, False) @@ -61,7 +61,7 @@ class LibrarythingImport(TestCase): self.assertEqual(import_items[2].data["Book Id"], "5015399") def test_create_retry_job(self): - """ trying again with items that didn't import """ + """trying again with items that didn't import""" import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") import_items = models.ImportItem.objects.filter(job=import_job).all()[:2] @@ -80,7 +80,7 @@ class LibrarythingImport(TestCase): @responses.activate def test_import_data(self): - """ resolve entry """ + """resolve entry""" import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") book = models.Edition.objects.create(title="Test Book") @@ -95,7 +95,7 @@ class LibrarythingImport(TestCase): self.assertEqual(import_item.book.id, book.id) def test_handle_imported_book(self): - """ librarything import added a book, this adds related connections """ + """librarything import added a book, this adds related connections""" shelf = self.user.shelf_set.filter(identifier="read").first() self.assertIsNone(shelf.books.first()) @@ -130,7 +130,7 @@ class LibrarythingImport(TestCase): self.assertEqual(readthrough.finish_date.day, 8) def test_handle_imported_book_already_shelved(self): - """ librarything import added a book, this adds related connections """ + """librarything import added a book, this adds related connections""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): shelf = self.user.shelf_set.filter(identifier="to-read").first() models.ShelfBook.objects.create(shelf=shelf, user=self.user, book=self.book) @@ -165,7 +165,7 @@ class LibrarythingImport(TestCase): self.assertEqual(readthrough.finish_date.day, 8) def test_handle_import_twice(self): - """ re-importing books """ + """re-importing books""" shelf = self.user.shelf_set.filter(identifier="read").first() import_job = models.ImportJob.objects.create(user=self.user) datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") @@ -202,7 +202,7 @@ class LibrarythingImport(TestCase): @patch("bookwyrm.activitystreams.ActivityStream.add_status") def test_handle_imported_book_review(self, _): - """ librarything review import """ + """librarything review import""" import_job = models.ImportJob.objects.create(user=self.user) datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") csv_file = open(datafile, "r", encoding=self.importer.encoding) @@ -225,7 +225,7 @@ class LibrarythingImport(TestCase): self.assertEqual(review.privacy, "unlisted") def test_handle_imported_book_reviews_disabled(self): - """ librarything review import """ + """librarything review import""" import_job = models.ImportJob.objects.create(user=self.user) datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") csv_file = open(datafile, "r", encoding=self.importer.encoding) diff --git a/bookwyrm/tests/management/__init__.py b/bookwyrm/tests/management/__init__.py new file mode 100644 index 00000000..b6e690fd --- /dev/null +++ b/bookwyrm/tests/management/__init__.py @@ -0,0 +1 @@ +from . import * diff --git a/bookwyrm/tests/management/test_populate_streams.py b/bookwyrm/tests/management/test_populate_streams.py new file mode 100644 index 00000000..d187c054 --- /dev/null +++ b/bookwyrm/tests/management/test_populate_streams.py @@ -0,0 +1,44 @@ +""" test populating user streams """ +from unittest.mock import patch +from django.test import TestCase + +from bookwyrm import models +from bookwyrm.management.commands.populate_streams import populate_streams + + +@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") +class Activitystreams(TestCase): + """using redis to build activity streams""" + + def setUp(self): + """we need some stuff""" + self.local_user = models.User.objects.create_user( + "mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse" + ) + self.another_user = models.User.objects.create_user( + "nutria", "nutria@nutria.nutria", "password", local=True, localname="nutria" + ) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + self.book = models.Edition.objects.create(title="test book") + + def test_populate_streams(self, _): + """make sure the function on the redis manager gets called""" + with patch("bookwyrm.activitystreams.ActivityStream.add_status"): + models.Comment.objects.create( + user=self.local_user, content="hi", book=self.book + ) + + with patch( + "bookwyrm.activitystreams.ActivityStream.populate_store" + ) as redis_mock: + populate_streams() + self.assertEqual(redis_mock.call_count, 6) # 2 users x 3 streams diff --git a/bookwyrm/tests/models/test_activitypub_mixin.py b/bookwyrm/tests/models/test_activitypub_mixin.py index 0d1acd97..1c0975c4 100644 --- a/bookwyrm/tests/models/test_activitypub_mixin.py +++ b/bookwyrm/tests/models/test_activitypub_mixin.py @@ -15,10 +15,10 @@ from bookwyrm.models.activitypub_mixin import ActivityMixin, ObjectMixin @patch("bookwyrm.activitystreams.ActivityStream.add_status") class ActivitypubMixins(TestCase): - """ functionality shared across models """ + """functionality shared across models""" def setUp(self): - """ shared data """ + """shared data""" self.local_user = models.User.objects.create_user( "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" ) @@ -46,16 +46,16 @@ class ActivitypubMixins(TestCase): # ActivitypubMixin def test_to_activity(self, _): - """ model to ActivityPub json """ + """model to ActivityPub json""" @dataclass(init=False) class TestActivity(ActivityObject): - """ real simple mock """ + """real simple mock""" type: str = "Test" class TestModel(ActivitypubMixin, base_model.BookWyrmModel): - """ real simple mock model because BookWyrmModel is abstract """ + """real simple mock model because BookWyrmModel is abstract""" instance = TestModel() instance.remote_id = "https://www.example.com/test" @@ -67,7 +67,7 @@ class ActivitypubMixins(TestCase): self.assertEqual(activity["type"], "Test") def test_find_existing_by_remote_id(self, _): - """ attempt to match a remote id to an object in the db """ + """attempt to match a remote id to an object in the db""" # uses a different remote id scheme # this isn't really part of this test directly but it's helpful to state book = models.Edition.objects.create( @@ -100,7 +100,7 @@ class ActivitypubMixins(TestCase): result = models.Status.find_existing_by_remote_id("https://comment.net") def test_find_existing(self, _): - """ match a blob of data to a model """ + """match a blob of data to a model""" book = models.Edition.objects.create( title="Test edition", openlibrary_key="OL1234", @@ -110,7 +110,7 @@ class ActivitypubMixins(TestCase): self.assertEqual(result, book) def test_get_recipients_public_object(self, _): - """ determines the recipients for an object's broadcast """ + """determines the recipients for an object's broadcast""" MockSelf = namedtuple("Self", ("privacy")) mock_self = MockSelf("public") recipients = ActivitypubMixin.get_recipients(mock_self) @@ -118,7 +118,7 @@ class ActivitypubMixins(TestCase): self.assertEqual(recipients[0], self.remote_user.inbox) def test_get_recipients_public_user_object_no_followers(self, _): - """ determines the recipients for a user's object broadcast """ + """determines the recipients for a user's object broadcast""" MockSelf = namedtuple("Self", ("privacy", "user")) mock_self = MockSelf("public", self.local_user) @@ -126,7 +126,7 @@ class ActivitypubMixins(TestCase): self.assertEqual(len(recipients), 0) def test_get_recipients_public_user_object(self, _): - """ determines the recipients for a user's object broadcast """ + """determines the recipients for a user's object broadcast""" MockSelf = namedtuple("Self", ("privacy", "user")) mock_self = MockSelf("public", self.local_user) self.local_user.followers.add(self.remote_user) @@ -136,7 +136,7 @@ class ActivitypubMixins(TestCase): self.assertEqual(recipients[0], self.remote_user.inbox) def test_get_recipients_public_user_object_with_mention(self, _): - """ determines the recipients for a user's object broadcast """ + """determines the recipients for a user's object broadcast""" MockSelf = namedtuple("Self", ("privacy", "user")) mock_self = MockSelf("public", self.local_user) self.local_user.followers.add(self.remote_user) @@ -155,11 +155,11 @@ class ActivitypubMixins(TestCase): recipients = ActivitypubMixin.get_recipients(mock_self) self.assertEqual(len(recipients), 2) - self.assertEqual(recipients[0], another_remote_user.inbox) - self.assertEqual(recipients[1], self.remote_user.inbox) + self.assertTrue(another_remote_user.inbox in recipients) + self.assertTrue(self.remote_user.inbox in recipients) def test_get_recipients_direct(self, _): - """ determines the recipients for a user's object broadcast """ + """determines the recipients for a user's object broadcast""" MockSelf = namedtuple("Self", ("privacy", "user")) mock_self = MockSelf("public", self.local_user) self.local_user.followers.add(self.remote_user) @@ -181,7 +181,7 @@ class ActivitypubMixins(TestCase): self.assertEqual(recipients[0], another_remote_user.inbox) def test_get_recipients_combine_inboxes(self, _): - """ should combine users with the same shared_inbox """ + """should combine users with the same shared_inbox""" self.remote_user.shared_inbox = "http://example.com/inbox" self.remote_user.save(broadcast=False) with patch("bookwyrm.models.user.set_remote_server.delay"): @@ -205,7 +205,7 @@ class ActivitypubMixins(TestCase): self.assertEqual(recipients[0], "http://example.com/inbox") def test_get_recipients_software(self, _): - """ should differentiate between bookwyrm and other remote users """ + """should differentiate between bookwyrm and other remote users""" with patch("bookwyrm.models.user.set_remote_server.delay"): another_remote_user = models.User.objects.create_user( "nutria", @@ -235,13 +235,13 @@ class ActivitypubMixins(TestCase): # ObjectMixin def test_object_save_create(self, _): - """ should save uneventufully when broadcast is disabled """ + """should save uneventufully when broadcast is disabled""" class Success(Exception): - """ this means we got to the right method """ + """this means we got to the right method""" class ObjectModel(ObjectMixin, base_model.BookWyrmModel): - """ real simple mock model because BookWyrmModel is abstract """ + """real simple mock model because BookWyrmModel is abstract""" user = models.fields.ForeignKey("User", on_delete=db.models.CASCADE) @@ -252,7 +252,7 @@ class ActivitypubMixins(TestCase): def broadcast( self, activity, sender, **kwargs ): # pylint: disable=arguments-differ - """ do something """ + """do something""" raise Success() def to_create_activity(self, user): # pylint: disable=arguments-differ @@ -266,13 +266,13 @@ class ActivitypubMixins(TestCase): ObjectModel(user=None).save() def test_object_save_update(self, _): - """ should save uneventufully when broadcast is disabled """ + """should save uneventufully when broadcast is disabled""" class Success(Exception): - """ this means we got to the right method """ + """this means we got to the right method""" class UpdateObjectModel(ObjectMixin, base_model.BookWyrmModel): - """ real simple mock model because BookWyrmModel is abstract """ + """real simple mock model because BookWyrmModel is abstract""" user = models.fields.ForeignKey("User", on_delete=db.models.CASCADE) last_edited_by = models.fields.ForeignKey( @@ -292,13 +292,13 @@ class ActivitypubMixins(TestCase): UpdateObjectModel(id=1, last_edited_by=self.local_user).save() def test_object_save_delete(self, _): - """ should create delete activities when objects are deleted by flag """ + """should create delete activities when objects are deleted by flag""" class ActivitySuccess(Exception): - """ this means we got to the right method """ + """this means we got to the right method""" class DeletableObjectModel(ObjectMixin, base_model.BookWyrmModel): - """ real simple mock model because BookWyrmModel is abstract """ + """real simple mock model because BookWyrmModel is abstract""" user = models.fields.ForeignKey("User", on_delete=db.models.CASCADE) deleted = models.fields.BooleanField() @@ -314,7 +314,7 @@ class ActivitypubMixins(TestCase): DeletableObjectModel(id=1, user=self.local_user, deleted=True).save() def test_to_delete_activity(self, _): - """ wrapper for Delete activity """ + """wrapper for Delete activity""" MockSelf = namedtuple("Self", ("remote_id", "to_activity")) mock_self = MockSelf( "https://example.com/status/1", lambda *args: self.object_mock @@ -329,7 +329,7 @@ class ActivitypubMixins(TestCase): ) def test_to_update_activity(self, _): - """ ditto above but for Update """ + """ditto above but for Update""" MockSelf = namedtuple("Self", ("remote_id", "to_activity")) mock_self = MockSelf( "https://example.com/status/1", lambda *args: self.object_mock @@ -347,7 +347,7 @@ class ActivitypubMixins(TestCase): # Activity mixin def test_to_undo_activity(self, _): - """ and again, for Undo """ + """and again, for Undo""" MockSelf = namedtuple("Self", ("remote_id", "to_activity", "user")) mock_self = MockSelf( "https://example.com/status/1", diff --git a/bookwyrm/tests/models/test_base_model.py b/bookwyrm/tests/models/test_base_model.py index 25a2e7ee..75d0444c 100644 --- a/bookwyrm/tests/models/test_base_model.py +++ b/bookwyrm/tests/models/test_base_model.py @@ -1,4 +1,5 @@ """ testing models """ +from unittest.mock import patch from django.test import TestCase from bookwyrm import models @@ -7,28 +8,44 @@ from bookwyrm.settings import DOMAIN class BaseModel(TestCase): - """ functionality shared across models """ + """functionality shared across models""" - def test_remote_id(self): - """ these should be generated """ - instance = base_model.BookWyrmModel() - instance.id = 1 - expected = instance.get_remote_id() - self.assertEqual(expected, "https://%s/bookwyrmmodel/1" % DOMAIN) - - def test_remote_id_with_user(self): - """ format of remote id when there's a user object """ - user = models.User.objects.create_user( + def setUp(self): + """shared data""" + self.local_user = models.User.objects.create_user( "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" ) - instance = base_model.BookWyrmModel() - instance.user = user - instance.id = 1 - expected = instance.get_remote_id() - self.assertEqual(expected, "https://%s/user/mouse/bookwyrmmodel/1" % DOMAIN) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + + class BookWyrmTestModel(base_model.BookWyrmModel): + """just making it not abstract""" + + self.test_model = BookWyrmTestModel() + + def test_remote_id(self): + """these should be generated""" + self.test_model.id = 1 + expected = self.test_model.get_remote_id() + self.assertEqual(expected, "https://%s/bookwyrmtestmodel/1" % DOMAIN) + + def test_remote_id_with_user(self): + """format of remote id when there's a user object""" + self.test_model.user = self.local_user + self.test_model.id = 1 + expected = self.test_model.get_remote_id() + self.assertEqual(expected, "https://%s/user/mouse/bookwyrmtestmodel/1" % DOMAIN) def test_set_remote_id(self): - """ this function sets remote ids after creation """ + """this function sets remote ids after creation""" # using Work because it BookWrymModel is abstract and this requires save # Work is a relatively not-fancy model. instance = models.Work.objects.create(title="work title") @@ -42,3 +59,66 @@ class BaseModel(TestCase): instance.remote_id = None base_model.set_remote_id(None, instance, False) self.assertIsNone(instance.remote_id) + + @patch("bookwyrm.activitystreams.ActivityStream.add_status") + def test_object_visible_to_user(self, _): + """does a user have permission to view an object""" + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="public" + ) + self.assertTrue(obj.visible_to_user(self.local_user)) + + obj = models.Shelf.objects.create( + name="test", user=self.remote_user, privacy="unlisted" + ) + self.assertTrue(obj.visible_to_user(self.local_user)) + + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="followers" + ) + self.assertFalse(obj.visible_to_user(self.local_user)) + + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="direct" + ) + self.assertFalse(obj.visible_to_user(self.local_user)) + + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="direct" + ) + obj.mention_users.add(self.local_user) + self.assertTrue(obj.visible_to_user(self.local_user)) + + @patch("bookwyrm.activitystreams.ActivityStream.add_status") + def test_object_visible_to_user_follower(self, _): + """what you can see if you follow a user""" + self.remote_user.followers.add(self.local_user) + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="followers" + ) + self.assertTrue(obj.visible_to_user(self.local_user)) + + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="direct" + ) + self.assertFalse(obj.visible_to_user(self.local_user)) + + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="direct" + ) + obj.mention_users.add(self.local_user) + self.assertTrue(obj.visible_to_user(self.local_user)) + + @patch("bookwyrm.activitystreams.ActivityStream.add_status") + def test_object_visible_to_user_blocked(self, _): + """you can't see it if they block you""" + self.remote_user.blocks.add(self.local_user) + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="public" + ) + self.assertFalse(obj.visible_to_user(self.local_user)) + + obj = models.Shelf.objects.create( + name="test", user=self.remote_user, privacy="unlisted" + ) + self.assertFalse(obj.visible_to_user(self.local_user)) diff --git a/bookwyrm/tests/models/test_book_model.py b/bookwyrm/tests/models/test_book_model.py index 14ab0c57..c80cc4a8 100644 --- a/bookwyrm/tests/models/test_book_model.py +++ b/bookwyrm/tests/models/test_book_model.py @@ -8,10 +8,10 @@ from bookwyrm.models.book import isbn_10_to_13, isbn_13_to_10 class Book(TestCase): - """ not too much going on in the books model but here we are """ + """not too much going on in the books model but here we are""" def setUp(self): - """ we'll need some books """ + """we'll need some books""" self.work = models.Work.objects.create( title="Example Work", remote_id="https://example.com/book/1" ) @@ -25,17 +25,17 @@ class Book(TestCase): ) def test_remote_id(self): - """ fanciness with remote/origin ids """ + """fanciness with remote/origin ids""" remote_id = "https://%s/book/%d" % (settings.DOMAIN, self.work.id) self.assertEqual(self.work.get_remote_id(), remote_id) self.assertEqual(self.work.remote_id, remote_id) def test_create_book(self): - """ you shouldn't be able to create Books (only editions and works) """ + """you shouldn't be able to create Books (only editions and works)""" self.assertRaises(ValueError, models.Book.objects.create, title="Invalid Book") def test_isbn_10_to_13(self): - """ checksums and so on """ + """checksums and so on""" isbn_10 = "178816167X" isbn_13 = isbn_10_to_13(isbn_10) self.assertEqual(isbn_13, "9781788161671") @@ -45,7 +45,7 @@ class Book(TestCase): self.assertEqual(isbn_13, "9781788161671") def test_isbn_13_to_10(self): - """ checksums and so on """ + """checksums and so on""" isbn_13 = "9781788161671" isbn_10 = isbn_13_to_10(isbn_13) self.assertEqual(isbn_10, "178816167X") @@ -55,7 +55,7 @@ class Book(TestCase): self.assertEqual(isbn_10, "178816167X") def test_get_edition_info(self): - """ text slug about an edition """ + """text slug about an edition""" book = models.Edition.objects.create(title="Test Edition") self.assertEqual(book.edition_info, "") @@ -77,7 +77,7 @@ class Book(TestCase): self.assertEqual(book.alt_text, "Test Edition (worm, Glorbish language, 2020)") def test_get_rank(self): - """ sets the data quality index for the book """ + """sets the data quality index for the book""" # basic rank self.assertEqual(self.first_edition.edition_rank, 0) diff --git a/bookwyrm/tests/models/test_federated_server.py b/bookwyrm/tests/models/test_federated_server.py new file mode 100644 index 00000000..43724568 --- /dev/null +++ b/bookwyrm/tests/models/test_federated_server.py @@ -0,0 +1,67 @@ +""" testing models """ +from unittest.mock import patch +from django.test import TestCase + +from bookwyrm import models + + +class FederatedServer(TestCase): + """federate server management""" + + def setUp(self): + """we'll need a user""" + self.server = models.FederatedServer.objects.create(server_name="test.server") + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + federated_server=self.server, + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + self.inactive_remote_user = models.User.objects.create_user( + "nutria", + "nutria@nutria.com", + "nutriaword", + federated_server=self.server, + local=False, + remote_id="https://example.com/users/nutria", + inbox="https://example.com/users/nutria/inbox", + outbox="https://example.com/users/nutria/outbox", + is_active=False, + deactivation_reason="self_deletion", + ) + + def test_block_unblock(self): + """block a server and all users on it""" + self.assertEqual(self.server.status, "federated") + self.assertTrue(self.remote_user.is_active) + self.assertFalse(self.inactive_remote_user.is_active) + + self.server.block() + + self.assertEqual(self.server.status, "blocked") + self.remote_user.refresh_from_db() + self.assertFalse(self.remote_user.is_active) + self.assertEqual(self.remote_user.deactivation_reason, "domain_block") + + self.inactive_remote_user.refresh_from_db() + self.assertFalse(self.inactive_remote_user.is_active) + self.assertEqual(self.inactive_remote_user.deactivation_reason, "self_deletion") + + # UNBLOCK + self.server.unblock() + + self.assertEqual(self.server.status, "federated") + # user blocked in deactivation is reactivated + self.remote_user.refresh_from_db() + self.assertTrue(self.remote_user.is_active) + self.assertIsNone(self.remote_user.deactivation_reason) + + # deleted user remains deleted + self.inactive_remote_user.refresh_from_db() + self.assertFalse(self.inactive_remote_user.is_active) + self.assertEqual(self.inactive_remote_user.deactivation_reason, "self_deletion") diff --git a/bookwyrm/tests/models/test_fields.py b/bookwyrm/tests/models/test_fields.py index 18bb028f..ea692b62 100644 --- a/bookwyrm/tests/models/test_fields.py +++ b/bookwyrm/tests/models/test_fields.py @@ -25,10 +25,10 @@ from bookwyrm.models.activitypub_mixin import ActivitypubMixin # pylint: disable=too-many-public-methods class ActivitypubFields(TestCase): - """ overwrites standard model feilds to work with activitypub """ + """overwrites standard model feilds to work with activitypub""" def test_validate_remote_id(self): - """ should look like a url """ + """should look like a url""" self.assertIsNone(fields.validate_remote_id("http://www.example.com")) self.assertIsNone(fields.validate_remote_id("https://www.example.com")) self.assertIsNone(fields.validate_remote_id("http://exle.com/dlg-23/x")) @@ -45,7 +45,7 @@ class ActivitypubFields(TestCase): ) def test_activitypub_field_mixin(self): - """ generic mixin with super basic to and from functionality """ + """generic mixin with super basic to and from functionality""" instance = fields.ActivitypubFieldMixin() self.assertEqual(instance.field_to_activity("fish"), "fish") self.assertEqual(instance.field_from_activity("fish"), "fish") @@ -63,11 +63,11 @@ class ActivitypubFields(TestCase): self.assertEqual(instance.get_activitypub_field(), "snakeCaseName") def test_set_field_from_activity(self): - """ setter from entire json blob """ + """setter from entire json blob""" @dataclass class TestModel: - """ real simple mock """ + """real simple mock""" field_name: str @@ -82,11 +82,11 @@ class ActivitypubFields(TestCase): self.assertEqual(mock_model.field_name, "hi") def test_set_activity_from_field(self): - """ set json field given entire model """ + """set json field given entire model""" @dataclass class TestModel: - """ real simple mock """ + """real simple mock""" field_name: str unrelated: str @@ -100,7 +100,7 @@ class ActivitypubFields(TestCase): self.assertEqual(data["fieldName"], "bip") def test_remote_id_field(self): - """ just sets some defaults on charfield """ + """just sets some defaults on charfield""" instance = fields.RemoteIdField() self.assertEqual(instance.max_length, 255) self.assertTrue(instance.deduplication_field) @@ -109,7 +109,7 @@ class ActivitypubFields(TestCase): instance.run_validators("http://www.example.com/dlfjg 23/x") def test_username_field(self): - """ again, just setting defaults on username field """ + """again, just setting defaults on username field""" instance = fields.UsernameField() self.assertEqual(instance.activitypub_field, "preferredUsername") self.assertEqual(instance.max_length, 150) @@ -130,7 +130,7 @@ class ActivitypubFields(TestCase): self.assertEqual(instance.field_to_activity("test@example.com"), "test") def test_privacy_field_defaults(self): - """ post privacy field's many default values """ + """post privacy field's many default values""" instance = fields.PrivacyField() self.assertEqual(instance.max_length, 255) self.assertEqual( @@ -143,11 +143,11 @@ class ActivitypubFields(TestCase): ) def test_privacy_field_set_field_from_activity(self): - """ translate between to/cc fields and privacy """ + """translate between to/cc fields and privacy""" @dataclass(init=False) class TestActivity(ActivityObject): - """ real simple mock """ + """real simple mock""" to: List[str] cc: List[str] @@ -155,7 +155,7 @@ class ActivitypubFields(TestCase): type: str = "Test" class TestPrivacyModel(ActivitypubMixin, BookWyrmModel): - """ real simple mock model because BookWyrmModel is abstract """ + """real simple mock model because BookWyrmModel is abstract""" privacy_field = fields.PrivacyField() mention_users = fields.TagField(User) @@ -187,7 +187,7 @@ class ActivitypubFields(TestCase): @patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast") @patch("bookwyrm.activitystreams.ActivityStream.add_status") def test_privacy_field_set_activity_from_field(self, *_): - """ translate between to/cc fields and privacy """ + """translate between to/cc fields and privacy""" user = User.objects.create_user( "rat", "rat@rat.rat", "ratword", local=True, localname="rat" ) @@ -231,7 +231,7 @@ class ActivitypubFields(TestCase): self.assertEqual(activity["cc"], []) def test_foreign_key(self): - """ should be able to format a related model """ + """should be able to format a related model""" instance = fields.ForeignKey("User", on_delete=models.CASCADE) Serializable = namedtuple("Serializable", ("to_activity", "remote_id")) item = Serializable(lambda: {"a": "b"}, "https://e.b/c") @@ -240,7 +240,7 @@ class ActivitypubFields(TestCase): @responses.activate def test_foreign_key_from_activity_str(self): - """ create a new object from a foreign key """ + """create a new object from a foreign key""" instance = fields.ForeignKey(User, on_delete=models.CASCADE) datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") userdata = json.loads(datafile.read_bytes()) @@ -264,7 +264,7 @@ class ActivitypubFields(TestCase): self.assertEqual(value.name, "MOUSE?? MOUSE!!") def test_foreign_key_from_activity_dict(self): - """ test recieving activity json """ + """test recieving activity json""" instance = fields.ForeignKey(User, on_delete=models.CASCADE) datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") userdata = json.loads(datafile.read_bytes()) @@ -284,7 +284,7 @@ class ActivitypubFields(TestCase): # et cetera but we're not testing serializing user json def test_foreign_key_from_activity_dict_existing(self): - """ test receiving a dict of an existing object in the db """ + """test receiving a dict of an existing object in the db""" instance = fields.ForeignKey(User, on_delete=models.CASCADE) datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") userdata = json.loads(datafile.read_bytes()) @@ -302,7 +302,7 @@ class ActivitypubFields(TestCase): self.assertEqual(value, user) def test_foreign_key_from_activity_str_existing(self): - """ test receiving a remote id of an existing object in the db """ + """test receiving a remote id of an existing object in the db""" instance = fields.ForeignKey(User, on_delete=models.CASCADE) user = User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" @@ -315,14 +315,14 @@ class ActivitypubFields(TestCase): self.assertEqual(value, user) def test_one_to_one_field(self): - """ a gussied up foreign key """ + """a gussied up foreign key""" instance = fields.OneToOneField("User", on_delete=models.CASCADE) Serializable = namedtuple("Serializable", ("to_activity", "remote_id")) item = Serializable(lambda: {"a": "b"}, "https://e.b/c") self.assertEqual(instance.field_to_activity(item), {"a": "b"}) def test_many_to_many_field(self): - """ lists! """ + """lists!""" instance = fields.ManyToManyField("User") Serializable = namedtuple("Serializable", ("to_activity", "remote_id")) @@ -340,7 +340,7 @@ class ActivitypubFields(TestCase): @responses.activate def test_many_to_many_field_from_activity(self): - """ resolve related fields for a list, takes a list of remote ids """ + """resolve related fields for a list, takes a list of remote ids""" instance = fields.ManyToManyField(User) datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") userdata = json.loads(datafile.read_bytes()) @@ -360,7 +360,7 @@ class ActivitypubFields(TestCase): self.assertIsInstance(value[0], User) def test_tag_field(self): - """ a special type of many to many field """ + """a special type of many to many field""" instance = fields.TagField("User") Serializable = namedtuple( @@ -379,13 +379,13 @@ class ActivitypubFields(TestCase): self.assertEqual(result[0].type, "Serializable") def test_tag_field_from_activity(self): - """ loadin' a list of items from Links """ + """loadin' a list of items from Links""" # TODO @responses.activate @patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast") def test_image_field(self, _): - """ storing images """ + """storing images""" user = User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" ) @@ -423,7 +423,7 @@ class ActivitypubFields(TestCase): self.assertIsInstance(loaded_image[1], ContentFile) def test_datetime_field(self): - """ this one is pretty simple, it just has to use isoformat """ + """this one is pretty simple, it just has to use isoformat""" instance = fields.DateTimeField() now = timezone.now() self.assertEqual(instance.field_to_activity(now), now.isoformat()) @@ -431,12 +431,12 @@ class ActivitypubFields(TestCase): self.assertEqual(instance.field_from_activity("bip"), None) def test_array_field(self): - """ idk why it makes them strings but probably for a good reason """ + """idk why it makes them strings but probably for a good reason""" instance = fields.ArrayField(fields.IntegerField) self.assertEqual(instance.field_to_activity([0, 1]), ["0", "1"]) def test_html_field(self): - """ sanitizes html, the sanitizer has its own tests """ + """sanitizes html, the sanitizer has its own tests""" instance = fields.HtmlField() self.assertEqual( instance.field_from_activity("

    hi

    "), "

    hi

    " diff --git a/bookwyrm/tests/models/test_import_model.py b/bookwyrm/tests/models/test_import_model.py index 38c3b1ed..76a914d1 100644 --- a/bookwyrm/tests/models/test_import_model.py +++ b/bookwyrm/tests/models/test_import_model.py @@ -14,10 +14,10 @@ from bookwyrm.connectors.abstract_connector import SearchResult class ImportJob(TestCase): - """ this is a fancy one!!! """ + """this is a fancy one!!!""" def setUp(self): - """ data is from a goodreads export of The Raven Tower """ + """data is from a goodreads export of The Raven Tower""" read_data = { "Book Id": 39395857, "Title": "The Raven Tower", @@ -72,30 +72,30 @@ class ImportJob(TestCase): ) def test_isbn(self): - """ it unquotes the isbn13 field from data """ + """it unquotes the isbn13 field from data""" expected = "9780356506999" item = models.ImportItem.objects.get(index=1) self.assertEqual(item.isbn, expected) def test_shelf(self): - """ converts to the local shelf typology """ + """converts to the local shelf typology""" expected = "reading" self.assertEqual(self.item_1.shelf, expected) def test_date_added(self): - """ converts to the local shelf typology """ + """converts to the local shelf typology""" expected = datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc) item = models.ImportItem.objects.get(index=1) self.assertEqual(item.date_added, expected) def test_date_read(self): - """ converts to the local shelf typology """ + """converts to the local shelf typology""" expected = datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc) item = models.ImportItem.objects.get(index=2) self.assertEqual(item.date_read, expected) def test_currently_reading_reads(self): - """ infer currently reading dates where available """ + """infer currently reading dates where available""" expected = [ models.ReadThrough( start_date=datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc) @@ -106,7 +106,7 @@ class ImportJob(TestCase): self.assertEqual(actual.reads[0].finish_date, expected[0].finish_date) def test_read_reads(self): - """ infer read dates where available """ + """infer read dates where available""" actual = self.item_2 self.assertEqual( actual.reads[0].start_date, @@ -118,14 +118,14 @@ class ImportJob(TestCase): ) def test_unread_reads(self): - """ handle books with no read dates """ + """handle books with no read dates""" expected = [] actual = models.ImportItem.objects.get(index=3) self.assertEqual(actual.reads, expected) @responses.activate def test_get_book_from_isbn(self): - """ search and load books by isbn (9780356506999) """ + """search and load books by isbn (9780356506999)""" connector_info = models.Connector.objects.create( identifier="openlibrary.org", name="OpenLibrary", diff --git a/bookwyrm/tests/models/test_list.py b/bookwyrm/tests/models/test_list.py index 6bc4b796..8f5bd497 100644 --- a/bookwyrm/tests/models/test_list.py +++ b/bookwyrm/tests/models/test_list.py @@ -7,49 +7,73 @@ from bookwyrm import models, settings @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class List(TestCase): - """ some activitypub oddness ahead """ + """some activitypub oddness ahead""" def setUp(self): - """ look, a list """ - self.user = models.User.objects.create_user( + """look, a list""" + self.local_user = models.User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" ) - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - self.list = models.List.objects.create(name="Test List", user=self.user) + work = models.Work.objects.create(title="hello") + self.book = models.Edition.objects.create(title="hi", parent_work=work) def test_remote_id(self, _): - """ shelves use custom remote ids """ - expected_id = "https://%s/list/%d" % (settings.DOMAIN, self.list.id) - self.assertEqual(self.list.get_remote_id(), expected_id) + """shelves use custom remote ids""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + book_list = models.List.objects.create( + name="Test List", user=self.local_user + ) + expected_id = "https://%s/list/%d" % (settings.DOMAIN, book_list.id) + self.assertEqual(book_list.get_remote_id(), expected_id) def test_to_activity(self, _): - """ jsonify it """ - activity_json = self.list.to_activity() + """jsonify it""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + book_list = models.List.objects.create( + name="Test List", user=self.local_user + ) + activity_json = book_list.to_activity() self.assertIsInstance(activity_json, dict) - self.assertEqual(activity_json["id"], self.list.remote_id) + self.assertEqual(activity_json["id"], book_list.remote_id) self.assertEqual(activity_json["totalItems"], 0) self.assertEqual(activity_json["type"], "BookList") self.assertEqual(activity_json["name"], "Test List") - self.assertEqual(activity_json["owner"], self.user.remote_id) + self.assertEqual(activity_json["owner"], self.local_user.remote_id) def test_list_item(self, _): - """ a list entry """ - work = models.Work.objects.create(title="hello") - book = models.Edition.objects.create(title="hi", parent_work=work) + """a list entry""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + book_list = models.List.objects.create( + name="Test List", user=self.local_user, privacy="unlisted" + ) + item = models.ListItem.objects.create( - book_list=self.list, - book=book, - user=self.user, + book_list=book_list, + book=self.book, + user=self.local_user, + order=1, ) self.assertTrue(item.approved) + self.assertEqual(item.privacy, "unlisted") + self.assertEqual(item.recipients, []) - add_activity = item.to_add_activity() - self.assertEqual(add_activity["actor"], self.user.remote_id) - self.assertEqual(add_activity["object"]["id"], book.remote_id) - self.assertEqual(add_activity["target"], self.list.remote_id) + def test_list_item_pending(self, _): + """a list entry""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + book_list = models.List.objects.create( + name="Test List", user=self.local_user + ) - remove_activity = item.to_remove_activity() - self.assertEqual(remove_activity["actor"], self.user.remote_id) - self.assertEqual(remove_activity["object"]["id"], book.remote_id) - self.assertEqual(remove_activity["target"], self.list.remote_id) + item = models.ListItem.objects.create( + book_list=book_list, + book=self.book, + user=self.local_user, + approved=False, + order=1, + ) + + self.assertFalse(item.approved) + self.assertEqual(item.book_list.privacy, "public") + self.assertEqual(item.privacy, "direct") + self.assertEqual(item.recipients, []) diff --git a/bookwyrm/tests/models/test_readthrough_model.py b/bookwyrm/tests/models/test_readthrough_model.py index f69e8779..93e9e654 100644 --- a/bookwyrm/tests/models/test_readthrough_model.py +++ b/bookwyrm/tests/models/test_readthrough_model.py @@ -6,10 +6,10 @@ from bookwyrm import models, settings class ReadThrough(TestCase): - """ some activitypub oddness ahead """ + """some activitypub oddness ahead""" def setUp(self): - """ look, a shelf """ + """look, a shelf""" self.user = models.User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" ) @@ -27,7 +27,7 @@ class ReadThrough(TestCase): ) def test_progress_update(self): - """ Test progress updates """ + """Test progress updates""" self.readthrough.create_update() # No-op, no progress yet self.readthrough.progress = 10 self.readthrough.create_update() diff --git a/bookwyrm/tests/models/test_relationship_models.py b/bookwyrm/tests/models/test_relationship_models.py index 0e842b21..d629b5c7 100644 --- a/bookwyrm/tests/models/test_relationship_models.py +++ b/bookwyrm/tests/models/test_relationship_models.py @@ -6,10 +6,10 @@ from bookwyrm import models class Relationship(TestCase): - """ following, blocking, stuff like that """ + """following, blocking, stuff like that""" def setUp(self): - """ we need some users for this """ + """we need some users for this""" with patch("bookwyrm.models.user.set_remote_server.delay"): self.remote_user = models.User.objects.create_user( "rat", @@ -27,11 +27,11 @@ class Relationship(TestCase): self.local_user.save(broadcast=False) def test_user_follows_from_request(self): - """ convert a follow request into a follow """ + """convert a follow request into a follow""" real_broadcast = models.UserFollowRequest.broadcast def mock_broadcast(_, activity, user): - """ introspect what's being sent out """ + """introspect what's being sent out""" self.assertEqual(user.remote_id, self.local_user.remote_id) self.assertEqual(activity["type"], "Follow") @@ -54,7 +54,7 @@ class Relationship(TestCase): models.UserFollowRequest.broadcast = real_broadcast def test_user_follows_from_request_custom_remote_id(self): - """ store a specific remote id for a relationship provided by remote """ + """store a specific remote id for a relationship provided by remote""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): request = models.UserFollowRequest.objects.create( user_subject=self.local_user, @@ -71,7 +71,7 @@ class Relationship(TestCase): self.assertEqual(rel.user_object, self.remote_user) def test_follow_request_activity(self): - """ accept a request and make it a relationship """ + """accept a request and make it a relationship""" real_broadcast = models.UserFollowRequest.broadcast def mock_broadcast(_, activity, user): @@ -88,7 +88,7 @@ class Relationship(TestCase): models.UserFollowRequest.broadcast = real_broadcast def test_follow_request_accept(self): - """ accept a request and make it a relationship """ + """accept a request and make it a relationship""" real_broadcast = models.UserFollowRequest.broadcast def mock_broadcast(_, activity, user): @@ -115,7 +115,7 @@ class Relationship(TestCase): models.UserFollowRequest.broadcast = real_broadcast def test_follow_request_reject(self): - """ accept a request and make it a relationship """ + """accept a request and make it a relationship""" real_broadcast = models.UserFollowRequest.broadcast def mock_reject(_, activity, user): diff --git a/bookwyrm/tests/models/test_shelf_model.py b/bookwyrm/tests/models/test_shelf_model.py index ebda0499..911df059 100644 --- a/bookwyrm/tests/models/test_shelf_model.py +++ b/bookwyrm/tests/models/test_shelf_model.py @@ -1,4 +1,6 @@ """ testing models """ +import json +from unittest.mock import patch from django.test import TestCase from bookwyrm import models, settings @@ -6,10 +8,10 @@ from bookwyrm import models, settings # pylint: disable=unused-argument class Shelf(TestCase): - """ some activitypub oddness ahead """ + """some activitypub oddness ahead""" def setUp(self): - """ look, a shelf """ + """look, a shelf""" self.local_user = models.User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" ) @@ -17,31 +19,20 @@ class Shelf(TestCase): self.book = models.Edition.objects.create(title="test book", parent_work=work) def test_remote_id(self): - """ shelves use custom remote ids """ - real_broadcast = models.Shelf.broadcast - - def broadcast_mock(_, activity, user, **kwargs): - """ nah """ - - models.Shelf.broadcast = broadcast_mock - shelf = models.Shelf.objects.create( - name="Test Shelf", identifier="test-shelf", user=self.local_user - ) + """shelves use custom remote ids""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + shelf = models.Shelf.objects.create( + name="Test Shelf", identifier="test-shelf", user=self.local_user + ) expected_id = "https://%s/user/mouse/books/test-shelf" % settings.DOMAIN self.assertEqual(shelf.get_remote_id(), expected_id) - models.Shelf.broadcast = real_broadcast def test_to_activity(self): - """ jsonify it """ - real_broadcast = models.Shelf.broadcast - - def empty_mock(_, activity, user, **kwargs): - """ nah """ - - models.Shelf.broadcast = empty_mock - shelf = models.Shelf.objects.create( - name="Test Shelf", identifier="test-shelf", user=self.local_user - ) + """jsonify it""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + shelf = models.Shelf.objects.create( + name="Test Shelf", identifier="test-shelf", user=self.local_user + ) activity_json = shelf.to_activity() self.assertIsInstance(activity_json, dict) self.assertEqual(activity_json["id"], shelf.remote_id) @@ -49,77 +40,53 @@ class Shelf(TestCase): self.assertEqual(activity_json["type"], "Shelf") self.assertEqual(activity_json["name"], "Test Shelf") self.assertEqual(activity_json["owner"], self.local_user.remote_id) - models.Shelf.broadcast = real_broadcast def test_create_update_shelf(self): - """ create and broadcast shelf creation """ - real_broadcast = models.Shelf.broadcast + """create and broadcast shelf creation""" - def create_mock(_, activity, user, **kwargs): - """ ok """ - self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity["type"], "Create") - self.assertEqual(activity["actor"], self.local_user.remote_id) - self.assertEqual(activity["object"]["name"], "Test Shelf") - - models.Shelf.broadcast = create_mock - - shelf = models.Shelf.objects.create( - name="Test Shelf", identifier="test-shelf", user=self.local_user - ) - - def update_mock(_, activity, user, **kwargs): - """ ok """ - self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity["type"], "Update") - self.assertEqual(activity["actor"], self.local_user.remote_id) - self.assertEqual(activity["object"]["name"], "arthur russel") - - models.Shelf.broadcast = update_mock + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + shelf = models.Shelf.objects.create( + name="Test Shelf", identifier="test-shelf", user=self.local_user + ) + activity = json.loads(mock.call_args[0][1]) + self.assertEqual(activity["type"], "Create") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["object"]["name"], "Test Shelf") shelf.name = "arthur russel" - shelf.save() + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + shelf.save() + activity = json.loads(mock.call_args[0][1]) + self.assertEqual(activity["type"], "Update") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["object"]["name"], "arthur russel") self.assertEqual(shelf.name, "arthur russel") - models.Shelf.broadcast = real_broadcast def test_shelve(self): - """ create and broadcast shelf creation """ - real_broadcast = models.Shelf.broadcast - real_shelfbook_broadcast = models.ShelfBook.broadcast + """create and broadcast shelf creation""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + shelf = models.Shelf.objects.create( + name="Test Shelf", identifier="test-shelf", user=self.local_user + ) - def add_mock(_, activity, user, **kwargs): - """ ok """ - self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity["type"], "Add") - self.assertEqual(activity["actor"], self.local_user.remote_id) - self.assertEqual(activity["object"]["id"], self.book.remote_id) - self.assertEqual(activity["target"], shelf.remote_id) - - def remove_mock(_, activity, user, **kwargs): - """ ok """ - self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity["type"], "Remove") - self.assertEqual(activity["actor"], self.local_user.remote_id) - self.assertEqual(activity["object"]["id"], self.book.remote_id) - self.assertEqual(activity["target"], shelf.remote_id) - - def empty_mock(_, activity, user, **kwargs): - """ nah """ - - models.Shelf.broadcast = empty_mock - shelf = models.Shelf.objects.create( - name="Test Shelf", identifier="test-shelf", user=self.local_user - ) - - models.ShelfBook.broadcast = add_mock - shelf_book = models.ShelfBook.objects.create( - shelf=shelf, user=self.local_user, book=self.book - ) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + shelf_book = models.ShelfBook.objects.create( + shelf=shelf, user=self.local_user, book=self.book + ) + self.assertEqual(mock.call_count, 1) + activity = json.loads(mock.call_args[0][1]) + self.assertEqual(activity["type"], "Add") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["object"]["id"], shelf_book.remote_id) + self.assertEqual(activity["target"], shelf.remote_id) self.assertEqual(shelf.books.first(), self.book) - models.ShelfBook.broadcast = remove_mock - shelf_book.delete() + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + shelf_book.delete() + self.assertEqual(mock.call_count, 1) + activity = json.loads(mock.call_args[0][1]) + self.assertEqual(activity["type"], "Remove") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["object"]["id"], shelf_book.remote_id) + self.assertEqual(activity["target"], shelf.remote_id) self.assertFalse(shelf.books.exists()) - - models.ShelfBook.broadcast = real_shelfbook_broadcast - models.Shelf.broadcast = real_broadcast diff --git a/bookwyrm/tests/models/test_status_model.py b/bookwyrm/tests/models/test_status_model.py index 208bf3ab..4c8930bc 100644 --- a/bookwyrm/tests/models/test_status_model.py +++ b/bookwyrm/tests/models/test_status_model.py @@ -17,10 +17,10 @@ from bookwyrm import activitypub, models, settings @patch("bookwyrm.models.Status.broadcast") @patch("bookwyrm.activitystreams.ActivityStream.add_status") class Status(TestCase): - """ lotta types of statuses """ + """lotta types of statuses""" def setUp(self): - """ useful things for creating a status """ + """useful things for creating a status""" self.local_user = models.User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" ) @@ -46,14 +46,14 @@ class Status(TestCase): self.book.cover.save("test.jpg", ContentFile(output.getvalue())) def test_status_generated_fields(self, *_): - """ setting remote id """ + """setting remote id""" status = models.Status.objects.create(content="bleh", user=self.local_user) expected_id = "https://%s/user/mouse/status/%d" % (settings.DOMAIN, status.id) self.assertEqual(status.remote_id, expected_id) self.assertEqual(status.privacy, "public") def test_replies(self, *_): - """ get a list of replies """ + """get a list of replies""" parent = models.Status.objects.create(content="hi", user=self.local_user) child = models.Status.objects.create( content="hello", reply_parent=parent, user=self.local_user @@ -72,7 +72,7 @@ class Status(TestCase): self.assertIsInstance(replies.last(), models.Review) def test_status_type(self, *_): - """ class name """ + """class name""" self.assertEqual(models.Status().status_type, "Note") self.assertEqual(models.Review().status_type, "Review") self.assertEqual(models.Quotation().status_type, "Quotation") @@ -80,14 +80,14 @@ class Status(TestCase): self.assertEqual(models.Boost().status_type, "Announce") def test_boostable(self, *_): - """ can a status be boosted, based on privacy """ + """can a status be boosted, based on privacy""" self.assertTrue(models.Status(privacy="public").boostable) self.assertTrue(models.Status(privacy="unlisted").boostable) self.assertFalse(models.Status(privacy="followers").boostable) self.assertFalse(models.Status(privacy="direct").boostable) def test_to_replies(self, *_): - """ activitypub replies collection """ + """activitypub replies collection""" parent = models.Status.objects.create(content="hi", user=self.local_user) child = models.Status.objects.create( content="hello", reply_parent=parent, user=self.local_user @@ -104,7 +104,7 @@ class Status(TestCase): self.assertEqual(replies["totalItems"], 2) def test_status_to_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Status.objects.create( content="test content", user=self.local_user ) @@ -115,8 +115,10 @@ class Status(TestCase): self.assertEqual(activity["sensitive"], False) def test_status_to_activity_tombstone(self, *_): - """ subclass of the base model version with a "pure" serializer """ - with patch("bookwyrm.activitystreams.ActivityStream.remove_status"): + """subclass of the base model version with a "pure" serializer""" + with patch( + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" + ): status = models.Status.objects.create( content="test content", user=self.local_user, @@ -129,7 +131,7 @@ class Status(TestCase): self.assertFalse(hasattr(activity, "content")) def test_status_to_pure_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Status.objects.create( content="test content", user=self.local_user ) @@ -141,7 +143,7 @@ class Status(TestCase): self.assertEqual(activity["attachment"], []) def test_generated_note_to_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.GeneratedNote.objects.create( content="test content", user=self.local_user ) @@ -155,7 +157,7 @@ class Status(TestCase): self.assertEqual(len(activity["tag"]), 2) def test_generated_note_to_pure_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.GeneratedNote.objects.create( content="test content", user=self.local_user ) @@ -179,7 +181,7 @@ class Status(TestCase): self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_comment_to_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Comment.objects.create( content="test content", user=self.local_user, book=self.book ) @@ -190,7 +192,7 @@ class Status(TestCase): self.assertEqual(activity["inReplyToBook"], self.book.remote_id) def test_comment_to_pure_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Comment.objects.create( content="test content", user=self.local_user, book=self.book ) @@ -210,7 +212,7 @@ class Status(TestCase): self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_quotation_to_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Quotation.objects.create( quote="a sickening sense", content="test content", @@ -225,7 +227,7 @@ class Status(TestCase): self.assertEqual(activity["inReplyToBook"], self.book.remote_id) def test_quotation_to_pure_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Quotation.objects.create( quote="a sickening sense", content="test content", @@ -248,7 +250,7 @@ class Status(TestCase): self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_review_to_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Review.objects.create( name="Review name", content="test content", @@ -265,9 +267,9 @@ class Status(TestCase): self.assertEqual(activity["inReplyToBook"], self.book.remote_id) def test_review_to_pure_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Review.objects.create( - name="Review name", + name="Review's name", content="test content", rating=3.0, user=self.local_user, @@ -278,7 +280,7 @@ class Status(TestCase): self.assertEqual(activity["type"], "Article") self.assertEqual( activity["name"], - 'Review of "%s" (3 stars): Review name' % self.book.title, + 'Review of "%s" (3 stars): Review\'s name' % self.book.title, ) self.assertEqual(activity["content"], "test content") self.assertEqual(activity["attachment"][0].type, "Document") @@ -289,7 +291,7 @@ class Status(TestCase): self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_review_to_pure_activity_no_rating(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Review.objects.create( name="Review name", content="test content", @@ -311,7 +313,7 @@ class Status(TestCase): self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_reviewrating_to_pure_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.ReviewRating.objects.create( rating=3.0, user=self.local_user, @@ -333,11 +335,11 @@ class Status(TestCase): self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_favorite(self, *_): - """ fav a status """ + """fav a status""" real_broadcast = models.Favorite.broadcast def fav_broadcast_mock(_, activity, user): - """ ok """ + """ok""" self.assertEqual(user.remote_id, self.local_user.remote_id) self.assertEqual(activity["type"], "Like") @@ -359,7 +361,7 @@ class Status(TestCase): models.Favorite.broadcast = real_broadcast def test_boost(self, *_): - """ boosting, this one's a bit fussy """ + """boosting, this one's a bit fussy""" status = models.Status.objects.create( content="test content", user=self.local_user ) @@ -371,7 +373,7 @@ class Status(TestCase): self.assertEqual(activity, boost.to_activity(pure=True)) def test_notification(self, *_): - """ a simple model """ + """a simple model""" notification = models.Notification.objects.create( user=self.local_user, notification_type="FAVORITE" ) @@ -383,7 +385,7 @@ class Status(TestCase): ) def test_create_broadcast(self, _, broadcast_mock): - """ should send out two verions of a status on create """ + """should send out two verions of a status on create""" models.Comment.objects.create( content="hi", user=self.local_user, book=self.book ) @@ -403,7 +405,7 @@ class Status(TestCase): self.assertEqual(args["object"]["type"], "Comment") def test_recipients_with_mentions(self, *_): - """ get recipients to broadcast a status """ + """get recipients to broadcast a status""" status = models.GeneratedNote.objects.create( content="test content", user=self.local_user ) @@ -412,7 +414,7 @@ class Status(TestCase): self.assertEqual(status.recipients, [self.remote_user]) def test_recipients_with_reply_parent(self, *_): - """ get recipients to broadcast a status """ + """get recipients to broadcast a status""" parent_status = models.GeneratedNote.objects.create( content="test content", user=self.remote_user ) @@ -423,7 +425,7 @@ class Status(TestCase): self.assertEqual(status.recipients, [self.remote_user]) def test_recipients_with_reply_parent_and_mentions(self, *_): - """ get recipients to broadcast a status """ + """get recipients to broadcast a status""" parent_status = models.GeneratedNote.objects.create( content="test content", user=self.remote_user ) @@ -436,7 +438,7 @@ class Status(TestCase): @responses.activate def test_ignore_activity_boost(self, *_): - """ don't bother with most remote statuses """ + """don't bother with most remote statuses""" activity = activitypub.Announce( id="http://www.faraway.com/boost/12", actor=self.remote_user.remote_id, diff --git a/bookwyrm/tests/models/test_user_model.py b/bookwyrm/tests/models/test_user_model.py index bd5255ce..b2791379 100644 --- a/bookwyrm/tests/models/test_user_model.py +++ b/bookwyrm/tests/models/test_user_model.py @@ -1,4 +1,5 @@ """ testing models """ +import json from unittest.mock import patch from django.test import TestCase import responses @@ -21,7 +22,7 @@ class User(TestCase): ) def test_computed_fields(self): - """ username instead of id here """ + """username instead of id here""" expected_id = "https://%s/user/mouse" % DOMAIN self.assertEqual(self.user.remote_id, expected_id) self.assertEqual(self.user.username, "mouse@%s" % DOMAIN) @@ -152,3 +153,17 @@ class User(TestCase): self.assertEqual(server.server_name, DOMAIN) self.assertIsNone(server.application_type) self.assertIsNone(server.application_version) + + def test_delete_user(self): + """deactivate a user""" + self.assertTrue(self.user.is_active) + with patch( + "bookwyrm.models.activitypub_mixin.broadcast_task.delay" + ) as broadcast_mock: + self.user.delete() + + self.assertEqual(broadcast_mock.call_count, 1) + activity = json.loads(broadcast_mock.call_args[0][1]) + self.assertEqual(activity["type"], "Delete") + self.assertEqual(activity["object"], self.user.remote_id) + self.assertFalse(self.user.is_active) diff --git a/bookwyrm/tests/test_activitystreams.py b/bookwyrm/tests/test_activitystreams.py index 88ca4693..59266383 100644 --- a/bookwyrm/tests/test_activitystreams.py +++ b/bookwyrm/tests/test_activitystreams.py @@ -7,10 +7,10 @@ from bookwyrm import activitystreams, models @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.activitystreams.ActivityStream.add_status") class Activitystreams(TestCase): - """ using redis to build activity streams """ + """using redis to build activity streams""" def setUp(self): - """ use a test csv """ + """use a test csv""" self.local_user = models.User.objects.create_user( "mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse" ) @@ -30,14 +30,14 @@ class Activitystreams(TestCase): self.book = models.Edition.objects.create(title="test book") class TestStream(activitystreams.ActivityStream): - """ test stream, don't have to do anything here """ + """test stream, don't have to do anything here""" key = "test" self.test_stream = TestStream() def test_activitystream_class_ids(self, *_): - """ the abstract base class for stream objects """ + """the abstract base class for stream objects""" self.assertEqual( self.test_stream.stream_id(self.local_user), "{}-test".format(self.local_user.id), @@ -47,26 +47,26 @@ class Activitystreams(TestCase): "{}-test-unread".format(self.local_user.id), ) - def test_abstractstream_stream_users(self, *_): - """ get a list of users that should see a status """ + def test_abstractstream_get_audience(self, *_): + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) - users = self.test_stream.stream_users(status) + users = self.test_stream.get_audience(status) # remote users don't have feeds self.assertFalse(self.remote_user in users) self.assertTrue(self.local_user in users) self.assertTrue(self.another_user in users) - def test_abstractstream_stream_users_direct(self, *_): - """ get a list of users that should see a status """ + def test_abstractstream_get_audience_direct(self, *_): + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="direct", ) status.mention_users.add(self.local_user) - users = self.test_stream.stream_users(status) + users = self.test_stream.get_audience(status) self.assertEqual(users, []) status = models.Comment.objects.create( @@ -76,36 +76,36 @@ class Activitystreams(TestCase): book=self.book, ) status.mention_users.add(self.local_user) - users = self.test_stream.stream_users(status) + users = self.test_stream.get_audience(status) self.assertTrue(self.local_user in users) self.assertFalse(self.another_user in users) self.assertFalse(self.remote_user in users) - def test_abstractstream_stream_users_followers_remote_user(self, *_): - """ get a list of users that should see a status """ + def test_abstractstream_get_audience_followers_remote_user(self, *_): + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="followers", ) - users = self.test_stream.stream_users(status) + users = self.test_stream.get_audience(status) self.assertFalse(users.exists()) - def test_abstractstream_stream_users_followers_self(self, *_): - """ get a list of users that should see a status """ + def test_abstractstream_get_audience_followers_self(self, *_): + """get a list of users that should see a status""" status = models.Comment.objects.create( user=self.local_user, content="hi", privacy="direct", book=self.book, ) - users = self.test_stream.stream_users(status) + users = self.test_stream.get_audience(status) self.assertTrue(self.local_user in users) self.assertFalse(self.another_user in users) self.assertFalse(self.remote_user in users) - def test_abstractstream_stream_users_followers_with_mention(self, *_): - """ get a list of users that should see a status """ + def test_abstractstream_get_audience_followers_with_mention(self, *_): + """get a list of users that should see a status""" status = models.Comment.objects.create( user=self.remote_user, content="hi", @@ -114,13 +114,13 @@ class Activitystreams(TestCase): ) status.mention_users.add(self.local_user) - users = self.test_stream.stream_users(status) + users = self.test_stream.get_audience(status) self.assertTrue(self.local_user in users) self.assertFalse(self.another_user in users) self.assertFalse(self.remote_user in users) - def test_abstractstream_stream_users_followers_with_relationship(self, *_): - """ get a list of users that should see a status """ + def test_abstractstream_get_audience_followers_with_relationship(self, *_): + """get a list of users that should see a status""" self.remote_user.followers.add(self.local_user) status = models.Comment.objects.create( user=self.remote_user, @@ -128,77 +128,77 @@ class Activitystreams(TestCase): privacy="direct", book=self.book, ) - users = self.test_stream.stream_users(status) + users = self.test_stream.get_audience(status) self.assertFalse(self.local_user in users) self.assertFalse(self.another_user in users) self.assertFalse(self.remote_user in users) - def test_homestream_stream_users(self, *_): - """ get a list of users that should see a status """ + def test_homestream_get_audience(self, *_): + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) - users = activitystreams.HomeStream().stream_users(status) + users = activitystreams.HomeStream().get_audience(status) self.assertFalse(users.exists()) - def test_homestream_stream_users_with_mentions(self, *_): - """ get a list of users that should see a status """ + def test_homestream_get_audience_with_mentions(self, *_): + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) status.mention_users.add(self.local_user) - users = activitystreams.HomeStream().stream_users(status) + users = activitystreams.HomeStream().get_audience(status) self.assertFalse(self.local_user in users) self.assertFalse(self.another_user in users) - def test_homestream_stream_users_with_relationship(self, *_): - """ get a list of users that should see a status """ + def test_homestream_get_audience_with_relationship(self, *_): + """get a list of users that should see a status""" self.remote_user.followers.add(self.local_user) status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) - users = activitystreams.HomeStream().stream_users(status) + users = activitystreams.HomeStream().get_audience(status) self.assertTrue(self.local_user in users) self.assertFalse(self.another_user in users) - def test_localstream_stream_users_remote_status(self, *_): - """ get a list of users that should see a status """ + def test_localstream_get_audience_remote_status(self, *_): + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) - users = activitystreams.LocalStream().stream_users(status) + users = activitystreams.LocalStream().get_audience(status) self.assertEqual(users, []) - def test_localstream_stream_users_local_status(self, *_): - """ get a list of users that should see a status """ + def test_localstream_get_audience_local_status(self, *_): + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.local_user, content="hi", privacy="public" ) - users = activitystreams.LocalStream().stream_users(status) + users = activitystreams.LocalStream().get_audience(status) self.assertTrue(self.local_user in users) self.assertTrue(self.another_user in users) - def test_localstream_stream_users_unlisted(self, *_): - """ get a list of users that should see a status """ + def test_localstream_get_audience_unlisted(self, *_): + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.local_user, content="hi", privacy="unlisted" ) - users = activitystreams.LocalStream().stream_users(status) + users = activitystreams.LocalStream().get_audience(status) self.assertEqual(users, []) - def test_federatedstream_stream_users(self, *_): - """ get a list of users that should see a status """ + def test_federatedstream_get_audience(self, *_): + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) - users = activitystreams.FederatedStream().stream_users(status) + users = activitystreams.FederatedStream().get_audience(status) self.assertTrue(self.local_user in users) self.assertTrue(self.another_user in users) - def test_federatedstream_stream_users_unlisted(self, *_): - """ get a list of users that should see a status """ + def test_federatedstream_get_audience_unlisted(self, *_): + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="unlisted" ) - users = activitystreams.FederatedStream().stream_users(status) + users = activitystreams.FederatedStream().get_audience(status) self.assertEqual(users, []) diff --git a/bookwyrm/tests/test_emailing.py b/bookwyrm/tests/test_emailing.py index 5d7d4894..0f9cc365 100644 --- a/bookwyrm/tests/test_emailing.py +++ b/bookwyrm/tests/test_emailing.py @@ -10,10 +10,10 @@ from bookwyrm import emailing, models @patch("bookwyrm.emailing.send_email.delay") class Emailing(TestCase): - """ every response to a get request, html or json """ + """every response to a get request, html or json""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -25,7 +25,7 @@ class Emailing(TestCase): models.SiteSettings.objects.create() def test_invite_email(self, email_mock): - """ load the invite email """ + """load the invite email""" invite_request = models.InviteRequest.objects.create( email="test@email.com", invite=models.SiteInvite.objects.create(user=self.local_user), @@ -40,7 +40,7 @@ class Emailing(TestCase): self.assertEqual(len(args), 4) def test_password_reset_email(self, email_mock): - """ load the password reset email """ + """load the password reset email""" reset = models.PasswordReset.objects.create(user=self.local_user) emailing.password_reset_email(reset) diff --git a/bookwyrm/tests/test_sanitize_html.py b/bookwyrm/tests/test_sanitize_html.py index 2b3d0378..6c405348 100644 --- a/bookwyrm/tests/test_sanitize_html.py +++ b/bookwyrm/tests/test_sanitize_html.py @@ -5,10 +5,10 @@ from bookwyrm.sanitize_html import InputHtmlParser class Sanitizer(TestCase): - """ sanitizer tests """ + """sanitizer tests""" def test_no_html(self): - """ just text """ + """just text""" input_text = "no html " parser = InputHtmlParser() parser.feed(input_text) @@ -16,7 +16,7 @@ class Sanitizer(TestCase): self.assertEqual(input_text, output) def test_valid_html(self): - """ leave the html untouched """ + """leave the html untouched""" input_text = "yes html" parser = InputHtmlParser() parser.feed(input_text) @@ -24,7 +24,7 @@ class Sanitizer(TestCase): self.assertEqual(input_text, output) def test_valid_html_attrs(self): - """ and don't remove attributes """ + """and don't remove attributes""" input_text = 'yes html' parser = InputHtmlParser() parser.feed(input_text) @@ -32,7 +32,7 @@ class Sanitizer(TestCase): self.assertEqual(input_text, output) def test_invalid_html(self): - """ remove all html when the html is malformed """ + """remove all html when the html is malformed""" input_text = "yes html" parser = InputHtmlParser() parser.feed(input_text) @@ -46,7 +46,7 @@ class Sanitizer(TestCase): self.assertEqual("yes html ", output) def test_disallowed_html(self): - """ remove disallowed html but keep allowed html """ + """remove disallowed html but keep allowed html""" input_text = "
    yes html
    " parser = InputHtmlParser() parser.feed(input_text) diff --git a/bookwyrm/tests/test_signing.py b/bookwyrm/tests/test_signing.py index d9cc411c..758ba9bb 100644 --- a/bookwyrm/tests/test_signing.py +++ b/bookwyrm/tests/test_signing.py @@ -20,7 +20,7 @@ from bookwyrm.signatures import create_key_pair, make_signature, make_digest def get_follow_activity(follower, followee): - """ generates a test activity """ + """generates a test activity""" return Follow( id="https://test.com/user/follow/id", actor=follower.remote_id, @@ -33,10 +33,10 @@ Sender = namedtuple("Sender", ("remote_id", "key_pair")) class Signature(TestCase): - """ signature test """ + """signature test""" def setUp(self): - """ create users and test data """ + """create users and test data""" self.mouse = models.User.objects.create_user( "mouse@%s" % DOMAIN, "mouse@example.com", "", local=True, localname="mouse" ) @@ -56,7 +56,7 @@ class Signature(TestCase): models.SiteSettings.objects.create() def send(self, signature, now, data, digest): - """ test request """ + """test request""" c = Client() return c.post( urlsplit(self.rat.inbox).path, @@ -74,7 +74,7 @@ class Signature(TestCase): def send_test_request( # pylint: disable=too-many-arguments self, sender, signer=None, send_data=None, digest=None, date=None ): - """ sends a follow request to the "rat" user """ + """sends a follow request to the "rat" user""" now = date or http_date() data = json.dumps(get_follow_activity(sender, self.rat)) digest = digest or make_digest(data) @@ -84,7 +84,7 @@ class Signature(TestCase): return self.send(signature, now, send_data or data, digest) def test_correct_signature(self): - """ this one should just work """ + """this one should just work""" response = self.send_test_request(sender=self.mouse) self.assertEqual(response.status_code, 200) @@ -96,7 +96,7 @@ class Signature(TestCase): @responses.activate def test_remote_signer(self): - """ signtures for remote users """ + """signtures for remote users""" datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json") data = json.loads(datafile.read_bytes()) data["id"] = self.fake_remote.remote_id @@ -119,7 +119,7 @@ class Signature(TestCase): @responses.activate def test_key_needs_refresh(self): - """ an out of date key should be updated and the new key work """ + """an out of date key should be updated and the new key work""" datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json") data = json.loads(datafile.read_bytes()) data["id"] = self.fake_remote.remote_id @@ -155,7 +155,7 @@ class Signature(TestCase): @responses.activate def test_nonexistent_signer(self): - """ fail when unable to look up signer """ + """fail when unable to look up signer""" responses.add( responses.GET, self.fake_remote.remote_id, @@ -177,7 +177,7 @@ class Signature(TestCase): @pytest.mark.integration def test_invalid_digest(self): - """ signature digest must be valid """ + """signature digest must be valid""" with patch("bookwyrm.activitypub.resolve_remote_id"): response = self.send_test_request( self.mouse, digest="SHA-256=AAAAAAAAAAAAAAAAAA" diff --git a/bookwyrm/tests/test_templatetags.py b/bookwyrm/tests/test_templatetags.py index 61136c2e..a92e887a 100644 --- a/bookwyrm/tests/test_templatetags.py +++ b/bookwyrm/tests/test_templatetags.py @@ -12,10 +12,10 @@ from bookwyrm.templatetags import bookwyrm_tags @patch("bookwyrm.activitystreams.ActivityStream.add_status") class TemplateTags(TestCase): - """ lotta different things here """ + """lotta different things here""" def setUp(self): - """ create some filler objects """ + """create some filler objects""" self.user = models.User.objects.create_user( "mouse@example.com", "mouse@mouse.mouse", @@ -34,34 +34,34 @@ class TemplateTags(TestCase): self.book = models.Edition.objects.create(title="Test Book") def test_dict_key(self, _): - """ just getting a value out of a dict """ + """just getting a value out of a dict""" test_dict = {"a": 1, "b": 3} self.assertEqual(bookwyrm_tags.dict_key(test_dict, "a"), 1) self.assertEqual(bookwyrm_tags.dict_key(test_dict, "c"), 0) def test_get_user_rating(self, _): - """ get a user's most recent rating of a book """ + """get a user's most recent rating of a book""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.Review.objects.create(user=self.user, book=self.book, rating=3) self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 3) def test_get_user_rating_doesnt_exist(self, _): - """ there is no rating available """ + """there is no rating available""" self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 0) def test_get_user_identifer_local(self, _): - """ fall back to the simplest uid available """ + """fall back to the simplest uid available""" self.assertNotEqual(self.user.username, self.user.localname) self.assertEqual(bookwyrm_tags.get_user_identifier(self.user), "mouse") def test_get_user_identifer_remote(self, _): - """ for a remote user, should be their full username """ + """for a remote user, should be their full username""" self.assertEqual( bookwyrm_tags.get_user_identifier(self.remote_user), "rat@example.com" ) def test_get_notification_count(self, _): - """ just countin' """ + """just countin'""" self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 0) models.Notification.objects.create(user=self.user, notification_type="FAVORITE") @@ -74,7 +74,7 @@ class TemplateTags(TestCase): self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 2) def test_get_replies(self, _): - """ direct replies to a status """ + """direct replies to a status""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): parent = models.Review.objects.create( user=self.user, book=self.book, content="hi" @@ -85,7 +85,9 @@ class TemplateTags(TestCase): second_child = models.Status.objects.create( reply_parent=parent, user=self.user, content="hi" ) - with patch("bookwyrm.activitystreams.ActivityStream.remove_status"): + with patch( + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" + ): third_child = models.Status.objects.create( reply_parent=parent, user=self.user, @@ -100,7 +102,7 @@ class TemplateTags(TestCase): self.assertFalse(third_child in replies) def test_get_parent(self, _): - """ get the reply parent of a status """ + """get the reply parent of a status""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): parent = models.Review.objects.create( user=self.user, book=self.book, content="hi" @@ -114,7 +116,7 @@ class TemplateTags(TestCase): self.assertIsInstance(result, models.Review) def test_get_user_liked(self, _): - """ did a user like a status """ + """did a user like a status""" status = models.Review.objects.create(user=self.remote_user, book=self.book) self.assertFalse(bookwyrm_tags.get_user_liked(self.user, status)) @@ -123,7 +125,7 @@ class TemplateTags(TestCase): self.assertTrue(bookwyrm_tags.get_user_liked(self.user, status)) def test_get_user_boosted(self, _): - """ did a user boost a status """ + """did a user boost a status""" status = models.Review.objects.create(user=self.remote_user, book=self.book) self.assertFalse(bookwyrm_tags.get_user_boosted(self.user, status)) @@ -132,7 +134,7 @@ class TemplateTags(TestCase): self.assertTrue(bookwyrm_tags.get_user_boosted(self.user, status)) def test_follow_request_exists(self, _): - """ does a user want to follow """ + """does a user want to follow""" self.assertFalse( bookwyrm_tags.follow_request_exists(self.user, self.remote_user) ) @@ -150,7 +152,7 @@ class TemplateTags(TestCase): ) def test_get_boosted(self, _): - """ load a boosted status """ + """load a boosted status""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Review.objects.create(user=self.remote_user, book=self.book) boost = models.Boost.objects.create(user=self.user, boosted_status=status) @@ -159,7 +161,7 @@ class TemplateTags(TestCase): self.assertEqual(boosted, status) def test_get_book_description(self, _): - """ grab it from the edition or the parent """ + """grab it from the edition or the parent""" work = models.Work.objects.create(title="Test Work") self.book.parent_work = work self.book.save() @@ -175,42 +177,12 @@ class TemplateTags(TestCase): self.assertEqual(bookwyrm_tags.get_book_description(self.book), "hello") def test_get_uuid(self, _): - """ uuid functionality """ + """uuid functionality""" uuid = bookwyrm_tags.get_uuid("hi") self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid)) - def test_time_since(self, _): - """ ultraconcise timestamps """ - self.assertEqual(bookwyrm_tags.time_since("bleh"), "") - - now = timezone.now() - self.assertEqual(bookwyrm_tags.time_since(now), "0s") - - seconds_ago = now - relativedelta(seconds=4) - self.assertEqual(bookwyrm_tags.time_since(seconds_ago), "4s") - - minutes_ago = now - relativedelta(minutes=8) - self.assertEqual(bookwyrm_tags.time_since(minutes_ago), "8m") - - hours_ago = now - relativedelta(hours=9) - self.assertEqual(bookwyrm_tags.time_since(hours_ago), "9h") - - days_ago = now - relativedelta(days=3) - self.assertEqual(bookwyrm_tags.time_since(days_ago), "3d") - - # I am not going to figure out how to mock dates tonight. - months_ago = now - relativedelta(months=5) - self.assertTrue( - re.match(r"[A-Z][a-z]{2} \d?\d", bookwyrm_tags.time_since(months_ago)) - ) - - years_ago = now - relativedelta(years=10) - self.assertTrue( - re.match(r"[A-Z][a-z]{2} \d?\d \d{4}", bookwyrm_tags.time_since(years_ago)) - ) - def test_get_markdown(self, _): - """ mardown format data """ + """mardown format data""" result = bookwyrm_tags.get_markdown("_hi_") self.assertEqual(result, "

    hi

    ") @@ -218,13 +190,13 @@ class TemplateTags(TestCase): self.assertEqual(result, "

    hi

    ") def test_get_mentions(self, _): - """ list of people mentioned """ + """list of people mentioned""" status = models.Status.objects.create(content="hi", user=self.remote_user) result = bookwyrm_tags.get_mentions(status, self.user) self.assertEqual(result, "@rat@example.com ") def test_get_status_preview_name(self, _): - """ status context string """ + """status context string""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create(content="hi", user=self.user) result = bookwyrm_tags.get_status_preview_name(status) @@ -249,7 +221,7 @@ class TemplateTags(TestCase): self.assertEqual(result, "quotation from Test Book") def test_related_status(self, _): - """ gets the subclass model for a notification status """ + """gets the subclass model for a notification status""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create(content="hi", user=self.user) notification = models.Notification.objects.create( diff --git a/bookwyrm/tests/views/inbox/__init__.py b/bookwyrm/tests/views/inbox/__init__.py new file mode 100644 index 00000000..b6e690fd --- /dev/null +++ b/bookwyrm/tests/views/inbox/__init__.py @@ -0,0 +1 @@ +from . import * diff --git a/bookwyrm/tests/views/inbox/test_inbox.py b/bookwyrm/tests/views/inbox/test_inbox.py new file mode 100644 index 00000000..697f4010 --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox.py @@ -0,0 +1,162 @@ +""" tests incoming activities""" +import json +import pathlib +from unittest.mock import patch + +from django.http import HttpResponseNotAllowed, HttpResponseNotFound +from django.test import TestCase, Client +from django.test.client import RequestFactory + +from bookwyrm import models, views + + +# pylint: disable=too-many-public-methods +class Inbox(TestCase): + """readthrough tests""" + + def setUp(self): + """basic user and book data""" + self.client = Client() + self.factory = RequestFactory() + local_user = models.User.objects.create_user( + "mouse@example.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + ) + local_user.remote_id = "https://example.com/user/mouse" + local_user.save(broadcast=False) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + self.create_json = { + "id": "hi", + "type": "Create", + "actor": "hi", + "to": ["https://www.w3.org/ns/activitystreams#public"], + "cc": ["https://example.com/user/mouse/followers"], + "object": {}, + } + models.SiteSettings.objects.create() + + def test_inbox_invalid_get(self): + """shouldn't try to handle if the user is not found""" + result = self.client.get("/inbox", content_type="application/json") + self.assertIsInstance(result, HttpResponseNotAllowed) + + def test_inbox_invalid_user(self): + """shouldn't try to handle if the user is not found""" + result = self.client.post( + "/user/bleh/inbox", + '{"type": "Test", "object": "exists"}', + content_type="application/json", + ) + self.assertIsInstance(result, HttpResponseNotFound) + + def test_inbox_invalid_bad_signature(self): + """bad request for invalid signature""" + with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: + mock_valid.return_value = False + result = self.client.post( + "/user/mouse/inbox", + '{"type": "Announce", "object": "exists"}', + content_type="application/json", + ) + self.assertEqual(result.status_code, 401) + + def test_inbox_invalid_bad_signature_delete(self): + """invalid signature for Delete is okay though""" + with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: + mock_valid.return_value = False + result = self.client.post( + "/user/mouse/inbox", + '{"type": "Delete", "object": "exists"}', + content_type="application/json", + ) + self.assertEqual(result.status_code, 200) + + def test_inbox_unknown_type(self): + """never heard of that activity type, don't have a handler for it""" + with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: + result = self.client.post( + "/inbox", + '{"type": "Fish", "object": "exists"}', + content_type="application/json", + ) + mock_valid.return_value = True + self.assertIsInstance(result, HttpResponseNotFound) + + def test_inbox_success(self): + """a known type, for which we start a task""" + activity = self.create_json + activity["object"] = { + "id": "https://example.com/list/22", + "type": "BookList", + "totalItems": 1, + "first": "https://example.com/list/22?page=1", + "last": "https://example.com/list/22?page=1", + "name": "Test List", + "owner": "https://example.com/user/mouse", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://example.com/user/mouse/followers"], + "summary": "summary text", + "curation": "curated", + "@context": "https://www.w3.org/ns/activitystreams", + } + with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: + mock_valid.return_value = True + + with patch("bookwyrm.views.inbox.activity_task.delay"): + result = self.client.post( + "/inbox", json.dumps(activity), content_type="application/json" + ) + self.assertEqual(result.status_code, 200) + + def test_is_blocked_user_agent(self): + """check for blocked servers""" + request = self.factory.post( + "", + HTTP_USER_AGENT="http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)", + ) + self.assertFalse(views.inbox.is_blocked_user_agent(request)) + + models.FederatedServer.objects.create( + server_name="mastodon.social", status="blocked" + ) + self.assertTrue(views.inbox.is_blocked_user_agent(request)) + + def test_is_blocked_activity(self): + """check for blocked servers""" + activity = {"actor": "https://mastodon.social/user/whaatever/else"} + self.assertFalse(views.inbox.is_blocked_activity(activity)) + + models.FederatedServer.objects.create( + server_name="mastodon.social", status="blocked" + ) + self.assertTrue(views.inbox.is_blocked_activity(activity)) + + def test_create_by_deactivated_user(self): + """don't let deactivated users post""" + self.remote_user.delete(broadcast=False) + self.assertTrue(self.remote_user.deleted) + datafile = pathlib.Path(__file__).parent.joinpath("../../data/ap_note.json") + status_data = json.loads(datafile.read_bytes()) + activity = self.create_json + activity["actor"] = self.remote_user.remote_id + activity["object"] = status_data + + with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: + mock_valid.return_value = True + + result = self.client.post( + "/inbox", json.dumps(activity), content_type="application/json" + ) + self.assertEqual(result.status_code, 403) diff --git a/bookwyrm/tests/views/inbox/test_inbox_add.py b/bookwyrm/tests/views/inbox/test_inbox_add.py new file mode 100644 index 00000000..9f237b6d --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_add.py @@ -0,0 +1,108 @@ +""" tests incoming activities""" +from unittest.mock import patch + +from django.test import TestCase +import responses + +from bookwyrm import models, views + + +# pylint: disable=too-many-public-methods +class InboxAdd(TestCase): + """inbox tests""" + + def setUp(self): + """basic user and book data""" + local_user = models.User.objects.create_user( + "mouse@example.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + ) + local_user.remote_id = "https://example.com/user/mouse" + local_user.save(broadcast=False) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + work = models.Work.objects.create(title="work title") + self.book = models.Edition.objects.create( + title="Test", + remote_id="https://bookwyrm.social/book/37292", + parent_work=work, + ) + + models.SiteSettings.objects.create() + + def test_handle_add_book_to_shelf(self): + """shelving a book""" + shelf = models.Shelf.objects.create(user=self.remote_user, name="Test Shelf") + shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read" + shelf.save() + + activity = { + "id": "https://bookwyrm.social/shelfbook/6189#add", + "type": "Add", + "actor": "https://example.com/users/rat", + "object": { + "actor": self.remote_user.remote_id, + "type": "ShelfItem", + "book": self.book.remote_id, + "id": "https://bookwyrm.social/shelfbook/6189", + }, + "target": "https://bookwyrm.social/user/mouse/shelf/to-read", + "@context": "https://www.w3.org/ns/activitystreams", + } + views.inbox.activity_task(activity) + self.assertEqual(shelf.books.first(), self.book) + + @responses.activate + def test_handle_add_book_to_list(self): + """listing a book""" + responses.add( + responses.GET, + "https://bookwyrm.social/user/mouse/list/to-read", + json={ + "id": "https://example.com/list/22", + "type": "BookList", + "totalItems": 1, + "first": "https://example.com/list/22?page=1", + "last": "https://example.com/list/22?page=1", + "name": "Test List", + "owner": "https://example.com/user/mouse", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://example.com/user/mouse/followers"], + "summary": "summary text", + "curation": "curated", + "@context": "https://www.w3.org/ns/activitystreams", + }, + ) + + activity = { + "id": "https://bookwyrm.social/listbook/6189#add", + "type": "Add", + "actor": "https://example.com/users/rat", + "object": { + "actor": self.remote_user.remote_id, + "type": "ListItem", + "book": self.book.remote_id, + "id": "https://bookwyrm.social/listbook/6189", + "order": 1, + }, + "target": "https://bookwyrm.social/user/mouse/list/to-read", + "@context": "https://www.w3.org/ns/activitystreams", + } + views.inbox.activity_task(activity) + + booklist = models.List.objects.get() + listitem = models.ListItem.objects.get() + self.assertEqual(booklist.name, "Test List") + self.assertEqual(booklist.books.first(), self.book) + self.assertEqual(listitem.remote_id, "https://bookwyrm.social/listbook/6189") diff --git a/bookwyrm/tests/views/inbox/test_inbox_announce.py b/bookwyrm/tests/views/inbox/test_inbox_announce.py new file mode 100644 index 00000000..4243dc78 --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_announce.py @@ -0,0 +1,201 @@ +""" tests incoming activities""" +from unittest.mock import patch + +from django.test import TestCase +import responses + +from bookwyrm import models, views + + +# pylint: disable=too-many-public-methods +class InboxActivities(TestCase): + """inbox tests""" + + def setUp(self): + """basic user and book data""" + self.local_user = models.User.objects.create_user( + "mouse@example.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + ) + self.local_user.remote_id = "https://example.com/user/mouse" + self.local_user.save(broadcast=False) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + with patch("bookwyrm.activitystreams.ActivityStream.add_status"): + self.status = models.Status.objects.create( + user=self.local_user, + content="Test status", + remote_id="https://example.com/status/1", + ) + + self.create_json = { + "id": "hi", + "type": "Create", + "actor": "hi", + "to": ["https://www.w3.org/ns/activitystreams#public"], + "cc": ["https://example.com/user/mouse/followers"], + "object": {}, + } + models.SiteSettings.objects.create() + + @patch("bookwyrm.activitystreams.ActivityStream.add_status") + def test_boost(self, redis_mock): + """boost a status""" + self.assertEqual(models.Notification.objects.count(), 0) + activity = { + "type": "Announce", + "id": "%s/boost" % self.status.remote_id, + "actor": self.remote_user.remote_id, + "object": self.status.remote_id, + "to": ["https://www.w3.org/ns/activitystreams#public"], + "cc": ["https://example.com/user/mouse/followers"], + "published": "Mon, 25 May 2020 19:31:20 GMT", + } + with patch("bookwyrm.models.status.Status.ignore_activity") as discarder: + discarder.return_value = False + views.inbox.activity_task(activity) + + # boost added to redis activitystreams + self.assertTrue(redis_mock.called) + + # boost created of correct status + boost = models.Boost.objects.get() + self.assertEqual(boost.boosted_status, self.status) + + # notification sent to original poster + notification = models.Notification.objects.get() + self.assertEqual(notification.user, self.local_user) + self.assertEqual(notification.related_status, self.status) + + @responses.activate + @patch("bookwyrm.activitystreams.ActivityStream.add_status") + def test_boost_remote_status(self, redis_mock): + """boost a status from a remote server""" + work = models.Work.objects.create(title="work title") + book = models.Edition.objects.create( + title="Test", + remote_id="https://bookwyrm.social/book/37292", + parent_work=work, + ) + self.assertEqual(models.Notification.objects.count(), 0) + activity = { + "type": "Announce", + "id": "%s/boost" % self.status.remote_id, + "actor": self.remote_user.remote_id, + "object": "https://remote.com/status/1", + "to": ["https://www.w3.org/ns/activitystreams#public"], + "cc": ["https://example.com/user/mouse/followers"], + "published": "Mon, 25 May 2020 19:31:20 GMT", + } + responses.add( + responses.GET, + "https://remote.com/status/1", + json={ + "id": "https://remote.com/status/1", + "type": "Comment", + "published": "2021-04-05T18:04:59.735190+00:00", + "attributedTo": self.remote_user.remote_id, + "content": "

    a comment

    ", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://b875df3d118b.ngrok.io/user/mouse/followers"], + "inReplyTo": "", + "inReplyToBook": book.remote_id, + "summary": "", + "tag": [], + "sensitive": False, + "@context": "https://www.w3.org/ns/activitystreams", + }, + ) + + with patch("bookwyrm.models.status.Status.ignore_activity") as discarder: + discarder.return_value = False + views.inbox.activity_task(activity) + self.assertTrue(redis_mock.called) + + boost = models.Boost.objects.get() + self.assertEqual(boost.boosted_status.remote_id, "https://remote.com/status/1") + self.assertEqual(boost.boosted_status.comment.status_type, "Comment") + self.assertEqual(boost.boosted_status.comment.book, book) + + @responses.activate + def test_discarded_boost(self): + """test a boost of a mastodon status that will be discarded""" + status = models.Status( + content="hi", + user=self.remote_user, + ) + with patch("bookwyrm.activitystreams.ActivityStream.add_status"): + status.save(broadcast=False) + activity = { + "type": "Announce", + "id": "http://www.faraway.com/boost/12", + "actor": self.remote_user.remote_id, + "object": status.remote_id, + "to": ["https://www.w3.org/ns/activitystreams#public"], + "cc": ["https://example.com/user/mouse/followers"], + "published": "Mon, 25 May 2020 19:31:20 GMT", + } + responses.add( + responses.GET, status.remote_id, json=status.to_activity(), status=200 + ) + views.inbox.activity_task(activity) + self.assertEqual(models.Boost.objects.count(), 0) + + def test_unboost(self): + """undo a boost""" + with patch("bookwyrm.activitystreams.ActivityStream.add_status"): + boost = models.Boost.objects.create( + boosted_status=self.status, user=self.remote_user + ) + activity = { + "type": "Undo", + "actor": "hi", + "id": "bleh", + "to": ["https://www.w3.org/ns/activitystreams#public"], + "cc": ["https://example.com/user/mouse/followers"], + "object": { + "type": "Announce", + "id": boost.remote_id, + "actor": self.remote_user.remote_id, + "object": self.status.remote_id, + "to": ["https://www.w3.org/ns/activitystreams#public"], + "cc": ["https://example.com/user/mouse/followers"], + "published": "Mon, 25 May 2020 19:31:20 GMT", + }, + } + with patch( + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" + ) as redis_mock: + views.inbox.activity_task(activity) + self.assertTrue(redis_mock.called) + self.assertFalse(models.Boost.objects.exists()) + + def test_unboost_unknown_boost(self): + """undo a boost""" + activity = { + "type": "Undo", + "actor": "hi", + "id": "bleh", + "to": ["https://www.w3.org/ns/activitystreams#public"], + "cc": ["https://example.com/user/mouse/followers"], + "object": { + "type": "Announce", + "id": "http://fake.com/unknown/boost", + "actor": self.remote_user.remote_id, + "object": self.status.remote_id, + "published": "Mon, 25 May 2020 19:31:20 GMT", + }, + } + views.inbox.activity_task(activity) diff --git a/bookwyrm/tests/views/inbox/test_inbox_block.py b/bookwyrm/tests/views/inbox/test_inbox_block.py new file mode 100644 index 00000000..956cf538 --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_block.py @@ -0,0 +1,98 @@ +""" tests incoming activities""" +from unittest.mock import patch + +from django.test import TestCase + +from bookwyrm import models, views + + +# pylint: disable=too-many-public-methods +class InboxBlock(TestCase): + """inbox tests""" + + def setUp(self): + """basic user and book data""" + self.local_user = models.User.objects.create_user( + "mouse@example.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + ) + self.local_user.remote_id = "https://example.com/user/mouse" + self.local_user.save(broadcast=False) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + + models.SiteSettings.objects.create() + + def test_handle_blocks(self): + """create a "block" database entry from an activity""" + self.local_user.followers.add(self.remote_user) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + models.UserFollowRequest.objects.create( + user_subject=self.local_user, user_object=self.remote_user + ) + self.assertTrue(models.UserFollows.objects.exists()) + self.assertTrue(models.UserFollowRequest.objects.exists()) + + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/9e1f41ac-9ddd-4159", + "type": "Block", + "actor": "https://example.com/users/rat", + "object": "https://example.com/user/mouse", + } + + with patch( + "bookwyrm.activitystreams.ActivityStream.remove_user_statuses" + ) as redis_mock: + views.inbox.activity_task(activity) + self.assertTrue(redis_mock.called) + views.inbox.activity_task(activity) + block = models.UserBlocks.objects.get() + self.assertEqual(block.user_subject, self.remote_user) + self.assertEqual(block.user_object, self.local_user) + self.assertEqual(block.remote_id, "https://example.com/9e1f41ac-9ddd-4159") + + self.assertFalse(models.UserFollows.objects.exists()) + self.assertFalse(models.UserFollowRequest.objects.exists()) + + def test_handle_unblock(self): + """unblock a user""" + self.remote_user.blocks.add(self.local_user) + + block = models.UserBlocks.objects.get() + block.remote_id = "https://example.com/9e1f41ac-9ddd-4159" + block.save() + + self.assertEqual(block.user_subject, self.remote_user) + self.assertEqual(block.user_object, self.local_user) + activity = { + "type": "Undo", + "actor": "hi", + "id": "bleh", + "to": ["https://www.w3.org/ns/activitystreams#public"], + "cc": ["https://example.com/user/mouse/followers"], + "object": { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/9e1f41ac-9ddd-4159", + "type": "Block", + "actor": "https://example.com/users/rat", + "object": "https://example.com/user/mouse", + }, + } + with patch( + "bookwyrm.activitystreams.ActivityStream.add_user_statuses" + ) as redis_mock: + views.inbox.activity_task(activity) + self.assertTrue(redis_mock.called) + self.assertFalse(models.UserBlocks.objects.exists()) diff --git a/bookwyrm/tests/views/inbox/test_inbox_create.py b/bookwyrm/tests/views/inbox/test_inbox_create.py new file mode 100644 index 00000000..e7a12024 --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_create.py @@ -0,0 +1,172 @@ +""" tests incoming activities""" +import json +import pathlib +from unittest.mock import patch + +from django.test import TestCase + +from bookwyrm import models, views +from bookwyrm.activitypub import ActivitySerializerError + + +# pylint: disable=too-many-public-methods +class InboxCreate(TestCase): + """readthrough tests""" + + def setUp(self): + """basic user and book data""" + self.local_user = models.User.objects.create_user( + "mouse@example.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + ) + self.local_user.remote_id = "https://example.com/user/mouse" + self.local_user.save(broadcast=False) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + with patch("bookwyrm.activitystreams.ActivityStream.add_status"): + self.status = models.Status.objects.create( + user=self.local_user, + content="Test status", + remote_id="https://example.com/status/1", + ) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + + self.create_json = { + "id": "hi", + "type": "Create", + "actor": "hi", + "to": ["https://www.w3.org/ns/activitystreams#public"], + "cc": ["https://example.com/user/mouse/followers"], + "object": {}, + } + models.SiteSettings.objects.create() + + def test_create_status(self): + """the "it justs works" mode""" + self.assertEqual(models.Status.objects.count(), 1) + + datafile = pathlib.Path(__file__).parent.joinpath( + "../../data/ap_quotation.json" + ) + status_data = json.loads(datafile.read_bytes()) + models.Edition.objects.create( + title="Test Book", remote_id="https://example.com/book/1" + ) + activity = self.create_json + activity["object"] = status_data + + with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock: + views.inbox.activity_task(activity) + self.assertTrue(redis_mock.called) + + status = models.Quotation.objects.get() + self.assertEqual( + status.remote_id, "https://example.com/user/mouse/quotation/13" + ) + self.assertEqual(status.quote, "quote body") + self.assertEqual(status.content, "commentary") + self.assertEqual(status.user, self.local_user) + self.assertEqual(models.Status.objects.count(), 2) + + # while we're here, lets ensure we avoid dupes + views.inbox.activity_task(activity) + self.assertEqual(models.Status.objects.count(), 2) + + def test_create_status_remote_note_with_mention(self): + """should only create it under the right circumstances""" + self.assertEqual(models.Status.objects.count(), 1) + self.assertFalse( + models.Notification.objects.filter(user=self.local_user).exists() + ) + + datafile = pathlib.Path(__file__).parent.joinpath("../../data/ap_note.json") + status_data = json.loads(datafile.read_bytes()) + activity = self.create_json + activity["object"] = status_data + + with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock: + views.inbox.activity_task(activity) + self.assertTrue(redis_mock.called) + status = models.Status.objects.last() + self.assertEqual(status.content, "test content in note") + self.assertEqual(status.mention_users.first(), self.local_user) + self.assertTrue( + models.Notification.objects.filter(user=self.local_user).exists() + ) + self.assertEqual(models.Notification.objects.get().notification_type, "MENTION") + + def test_create_status_remote_note_with_reply(self): + """should only create it under the right circumstances""" + self.assertEqual(models.Status.objects.count(), 1) + self.assertFalse(models.Notification.objects.filter(user=self.local_user)) + + datafile = pathlib.Path(__file__).parent.joinpath("../../data/ap_note.json") + status_data = json.loads(datafile.read_bytes()) + del status_data["tag"] + status_data["inReplyTo"] = self.status.remote_id + activity = self.create_json + activity["object"] = status_data + + with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock: + views.inbox.activity_task(activity) + self.assertTrue(redis_mock.called) + status = models.Status.objects.last() + self.assertEqual(status.content, "test content in note") + self.assertEqual(status.reply_parent, self.status) + self.assertTrue(models.Notification.objects.filter(user=self.local_user)) + self.assertEqual(models.Notification.objects.get().notification_type, "REPLY") + + def test_create_list(self): + """a new list""" + activity = self.create_json + activity["object"] = { + "id": "https://example.com/list/22", + "type": "BookList", + "totalItems": 1, + "first": "https://example.com/list/22?page=1", + "last": "https://example.com/list/22?page=1", + "name": "Test List", + "owner": "https://example.com/user/mouse", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://example.com/user/mouse/followers"], + "summary": "summary text", + "curation": "curated", + "@context": "https://www.w3.org/ns/activitystreams", + } + views.inbox.activity_task(activity) + book_list = models.List.objects.get() + self.assertEqual(book_list.name, "Test List") + self.assertEqual(book_list.curation, "curated") + self.assertEqual(book_list.description, "summary text") + self.assertEqual(book_list.remote_id, "https://example.com/list/22") + + def test_create_unsupported_type(self): + """ignore activities we know we can't handle""" + activity = self.create_json + activity["object"] = { + "id": "https://example.com/status/887", + "type": "Question", + } + # just observer how it doesn't throw an error + views.inbox.activity_task(activity) + + def test_create_unknown_type(self): + """ignore activities we know we've never heard of""" + activity = self.create_json + activity["object"] = { + "id": "https://example.com/status/887", + "type": "Threnody", + } + with self.assertRaises(ActivitySerializerError): + views.inbox.activity_task(activity) diff --git a/bookwyrm/tests/views/inbox/test_inbox_delete.py b/bookwyrm/tests/views/inbox/test_inbox_delete.py new file mode 100644 index 00000000..617dcf6f --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_delete.py @@ -0,0 +1,137 @@ +""" tests incoming activities""" +from datetime import datetime +from unittest.mock import patch + +from django.test import TestCase + +from bookwyrm import models, views + + +# pylint: disable=too-many-public-methods +class InboxActivities(TestCase): + """inbox tests""" + + def setUp(self): + """basic user and book data""" + self.local_user = models.User.objects.create_user( + "mouse@example.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + ) + self.local_user.remote_id = "https://example.com/user/mouse" + self.local_user.save(broadcast=False) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + with patch("bookwyrm.activitystreams.ActivityStream.add_status"): + self.status = models.Status.objects.create( + user=self.remote_user, + content="Test status", + remote_id="https://example.com/status/1", + ) + + self.create_json = { + "id": "hi", + "type": "Create", + "actor": "hi", + "to": ["https://www.w3.org/ns/activitystreams#public"], + "cc": ["https://example.com/user/mouse/followers"], + "object": {}, + } + models.SiteSettings.objects.create() + + def test_delete_status(self): + """remove a status""" + self.assertFalse(self.status.deleted) + activity = { + "type": "Delete", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://example.com/user/mouse/followers"], + "id": "%s/activity" % self.status.remote_id, + "actor": self.remote_user.remote_id, + "object": {"id": self.status.remote_id, "type": "Tombstone"}, + } + with patch( + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" + ) as redis_mock: + views.inbox.activity_task(activity) + self.assertTrue(redis_mock.called) + # deletion doens't remove the status, it turns it into a tombstone + status = models.Status.objects.get() + self.assertTrue(status.deleted) + self.assertIsInstance(status.deleted_date, datetime) + + def test_delete_status_notifications(self): + """remove a status with related notifications""" + models.Notification.objects.create( + related_status=self.status, + user=self.local_user, + notification_type="MENTION", + ) + # this one is innocent, don't delete it + notif = models.Notification.objects.create( + user=self.local_user, notification_type="MENTION" + ) + self.assertFalse(self.status.deleted) + self.assertEqual(models.Notification.objects.count(), 2) + activity = { + "type": "Delete", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://example.com/user/mouse/followers"], + "id": "%s/activity" % self.status.remote_id, + "actor": self.remote_user.remote_id, + "object": {"id": self.status.remote_id, "type": "Tombstone"}, + } + with patch( + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" + ) as redis_mock: + views.inbox.activity_task(activity) + self.assertTrue(redis_mock.called) + # deletion doens't remove the status, it turns it into a tombstone + status = models.Status.objects.get() + self.assertTrue(status.deleted) + self.assertIsInstance(status.deleted_date, datetime) + + # notifications should be truly deleted + self.assertEqual(models.Notification.objects.count(), 1) + self.assertEqual(models.Notification.objects.get(), notif) + + def test_delete_user(self): + """delete a user""" + self.assertTrue(models.User.objects.get(username="rat@example.com").is_active) + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/users/test-user#delete", + "type": "Delete", + "actor": "https://example.com/users/test-user", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "object": self.remote_user.remote_id, + } + + views.inbox.activity_task(activity) + self.assertFalse(models.User.objects.get(username="rat@example.com").is_active) + + def test_delete_user_unknown(self): + """don't worry about it if we don't know the user""" + self.assertEqual(models.User.objects.filter(is_active=True).count(), 2) + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/users/test-user#delete", + "type": "Delete", + "actor": "https://example.com/users/test-user", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "object": "https://example.com/users/test-user", + } + + # nothing happens. + views.inbox.activity_task(activity) + self.assertEqual(models.User.objects.filter(is_active=True).count(), 2) diff --git a/bookwyrm/tests/views/inbox/test_inbox_follow.py b/bookwyrm/tests/views/inbox/test_inbox_follow.py new file mode 100644 index 00000000..f5332b7a --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_follow.py @@ -0,0 +1,235 @@ +""" tests incoming activities""" +import json +from unittest.mock import patch + +from django.test import TestCase + +from bookwyrm import models, views + + +# pylint: disable=too-many-public-methods +class InboxRelationships(TestCase): + """inbox tests""" + + def setUp(self): + """basic user and book data""" + self.local_user = models.User.objects.create_user( + "mouse@example.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + ) + self.local_user.remote_id = "https://example.com/user/mouse" + self.local_user.save(broadcast=False) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + + models.SiteSettings.objects.create() + + def test_follow(self): + """remote user wants to follow local user""" + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/users/rat/follows/123", + "type": "Follow", + "actor": "https://example.com/users/rat", + "object": "https://example.com/user/mouse", + } + + self.assertFalse(models.UserFollowRequest.objects.exists()) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + views.inbox.activity_task(activity) + self.assertEqual(mock.call_count, 1) + response_activity = json.loads(mock.call_args[0][1]) + self.assertEqual(response_activity["type"], "Accept") + + # notification created + notification = models.Notification.objects.get() + self.assertEqual(notification.user, self.local_user) + self.assertEqual(notification.notification_type, "FOLLOW") + + # the request should have been deleted + self.assertFalse(models.UserFollowRequest.objects.exists()) + + # the follow relationship should exist + follow = models.UserFollows.objects.get(user_object=self.local_user) + self.assertEqual(follow.user_subject, self.remote_user) + + def test_follow_duplicate(self): + """remote user wants to follow local user twice""" + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/users/rat/follows/123", + "type": "Follow", + "actor": "https://example.com/users/rat", + "object": "https://example.com/user/mouse", + } + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.inbox.activity_task(activity) + + # the follow relationship should exist + follow = models.UserFollows.objects.get(user_object=self.local_user) + self.assertEqual(follow.user_subject, self.remote_user) + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + views.inbox.activity_task(activity) + self.assertEqual(mock.call_count, 1) + response_activity = json.loads(mock.call_args[0][1]) + self.assertEqual(response_activity["type"], "Accept") + + # the follow relationship should STILL exist + follow = models.UserFollows.objects.get(user_object=self.local_user) + self.assertEqual(follow.user_subject, self.remote_user) + + def test_follow_manually_approved(self): + """needs approval before following""" + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/users/rat/follows/123", + "type": "Follow", + "actor": "https://example.com/users/rat", + "object": "https://example.com/user/mouse", + } + + self.local_user.manually_approves_followers = True + self.local_user.save(broadcast=False) + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.inbox.activity_task(activity) + + # notification created + notification = models.Notification.objects.get() + self.assertEqual(notification.user, self.local_user) + self.assertEqual(notification.notification_type, "FOLLOW_REQUEST") + + # the request should exist + request = models.UserFollowRequest.objects.get() + self.assertEqual(request.user_subject, self.remote_user) + self.assertEqual(request.user_object, self.local_user) + + # the follow relationship should not exist + follow = models.UserFollows.objects.all() + self.assertEqual(list(follow), []) + + def test_undo_follow_request(self): + """the requester cancels a follow request""" + self.local_user.manually_approves_followers = True + self.local_user.save(broadcast=False) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + request = models.UserFollowRequest.objects.create( + user_subject=self.remote_user, user_object=self.local_user + ) + self.assertTrue(self.local_user.follower_requests.exists()) + + activity = { + "type": "Undo", + "id": "bleh", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://example.com/user/mouse/followers"], + "actor": self.remote_user.remote_id, + "@context": "https://www.w3.org/ns/activitystreams", + "object": { + "@context": "https://www.w3.org/ns/activitystreams", + "id": request.remote_id, + "type": "Follow", + "actor": "https://example.com/users/rat", + "object": "https://example.com/user/mouse", + }, + } + + views.inbox.activity_task(activity) + + self.assertFalse(self.local_user.follower_requests.exists()) + + def test_unfollow(self): + """remove a relationship""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + rel = models.UserFollows.objects.create( + user_subject=self.remote_user, user_object=self.local_user + ) + activity = { + "type": "Undo", + "id": "bleh", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://example.com/user/mouse/followers"], + "actor": self.remote_user.remote_id, + "@context": "https://www.w3.org/ns/activitystreams", + "object": { + "id": rel.remote_id, + "type": "Follow", + "actor": "https://example.com/users/rat", + "object": "https://example.com/user/mouse", + }, + } + self.assertEqual(self.remote_user, self.local_user.followers.first()) + + views.inbox.activity_task(activity) + self.assertIsNone(self.local_user.followers.first()) + + def test_follow_accept(self): + """a remote user approved a follow request from local""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + rel = models.UserFollowRequest.objects.create( + user_subject=self.local_user, user_object=self.remote_user + ) + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/users/rat/follows/123#accepts", + "type": "Accept", + "actor": "https://example.com/users/rat", + "object": { + "id": rel.remote_id, + "type": "Follow", + "actor": "https://example.com/user/mouse", + "object": "https://example.com/users/rat", + }, + } + + self.assertEqual(models.UserFollowRequest.objects.count(), 1) + + views.inbox.activity_task(activity) + + # request should be deleted + self.assertEqual(models.UserFollowRequest.objects.count(), 0) + + # relationship should be created + follows = self.remote_user.followers + self.assertEqual(follows.count(), 1) + self.assertEqual(follows.first(), self.local_user) + + def test_follow_reject(self): + """turn down a follow request""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + rel = models.UserFollowRequest.objects.create( + user_subject=self.local_user, user_object=self.remote_user + ) + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/users/rat/follows/123#accepts", + "type": "Reject", + "actor": "https://example.com/users/rat", + "object": { + "id": rel.remote_id, + "type": "Follow", + "actor": "https://example.com/user/mouse", + "object": "https://example.com/users/rat", + }, + } + + self.assertEqual(models.UserFollowRequest.objects.count(), 1) + + views.inbox.activity_task(activity) + + # request should be deleted + self.assertFalse(models.UserFollowRequest.objects.exists()) + self.assertFalse(self.remote_user.followers.exists()) diff --git a/bookwyrm/tests/views/inbox/test_inbox_like.py b/bookwyrm/tests/views/inbox/test_inbox_like.py new file mode 100644 index 00000000..a5f3a9f0 --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_like.py @@ -0,0 +1,110 @@ +""" tests incoming activities""" +from unittest.mock import patch + +from django.test import TestCase + +from bookwyrm import models, views + + +# pylint: disable=too-many-public-methods +class InboxActivities(TestCase): + """inbox tests""" + + def setUp(self): + """basic user and book data""" + self.local_user = models.User.objects.create_user( + "mouse@example.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + ) + self.local_user.remote_id = "https://example.com/user/mouse" + self.local_user.save(broadcast=False) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + with patch("bookwyrm.activitystreams.ActivityStream.add_status"): + self.status = models.Status.objects.create( + user=self.local_user, + content="Test status", + remote_id="https://example.com/status/1", + ) + + self.create_json = { + "id": "hi", + "type": "Create", + "actor": "hi", + "to": ["https://www.w3.org/ns/activitystreams#public"], + "cc": ["https://example.com/user/mouse/followers"], + "object": {}, + } + models.SiteSettings.objects.create() + + def test_handle_favorite(self): + """fav a status""" + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/fav/1", + "actor": "https://example.com/users/rat", + "type": "Like", + "published": "Mon, 25 May 2020 19:31:20 GMT", + "object": self.status.remote_id, + } + + views.inbox.activity_task(activity) + + fav = models.Favorite.objects.get(remote_id="https://example.com/fav/1") + self.assertEqual(fav.status, self.status) + self.assertEqual(fav.remote_id, "https://example.com/fav/1") + self.assertEqual(fav.user, self.remote_user) + + def test_ignore_favorite(self): + """don't try to save an unknown status""" + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/fav/1", + "actor": "https://example.com/users/rat", + "type": "Like", + "published": "Mon, 25 May 2020 19:31:20 GMT", + "object": "https://unknown.status/not-found", + } + + views.inbox.activity_task(activity) + + self.assertFalse(models.Favorite.objects.exists()) + + def test_handle_unfavorite(self): + """fav a status""" + activity = { + "id": "https://example.com/fav/1#undo", + "type": "Undo", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://example.com/user/mouse/followers"], + "actor": self.remote_user.remote_id, + "object": { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/fav/1", + "actor": "https://example.com/users/rat", + "type": "Like", + "published": "Mon, 25 May 2020 19:31:20 GMT", + "object": self.status.remote_id, + }, + } + models.Favorite.objects.create( + status=self.status, + user=self.remote_user, + remote_id="https://example.com/fav/1", + ) + self.assertEqual(models.Favorite.objects.count(), 1) + + views.inbox.activity_task(activity) + self.assertEqual(models.Favorite.objects.count(), 0) diff --git a/bookwyrm/tests/views/inbox/test_inbox_remove.py b/bookwyrm/tests/views/inbox/test_inbox_remove.py new file mode 100644 index 00000000..a0c4cdcf --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_remove.py @@ -0,0 +1,102 @@ +""" tests incoming activities""" +from unittest.mock import patch + +from django.test import TestCase + +from bookwyrm import models, views + + +# pylint: disable=too-many-public-methods +class InboxRemove(TestCase): + """inbox tests""" + + def setUp(self): + """basic user and book data""" + self.local_user = models.User.objects.create_user( + "mouse@example.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + ) + self.local_user.remote_id = "https://example.com/user/mouse" + self.local_user.save(broadcast=False) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + self.work = models.Work.objects.create(title="work title") + self.book = models.Edition.objects.create( + title="Test", + remote_id="https://bookwyrm.social/book/37292", + parent_work=self.work, + ) + + models.SiteSettings.objects.create() + + def test_handle_unshelve_book(self): + """remove a book from a shelf""" + shelf = models.Shelf.objects.create(user=self.remote_user, name="Test Shelf") + shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read" + shelf.save() + + shelfbook = models.ShelfBook.objects.create( + user=self.remote_user, shelf=shelf, book=self.book + ) + + self.assertEqual(shelf.books.first(), self.book) + self.assertEqual(shelf.books.count(), 1) + + activity = { + "id": shelfbook.remote_id, + "type": "Remove", + "actor": "https://example.com/users/rat", + "object": { + "actor": self.remote_user.remote_id, + "type": "ShelfItem", + "book": self.book.remote_id, + "id": shelfbook.remote_id, + }, + "target": "https://bookwyrm.social/user/mouse/shelf/to-read", + "@context": "https://www.w3.org/ns/activitystreams", + } + views.inbox.activity_task(activity) + self.assertFalse(shelf.books.exists()) + + def test_handle_remove_book_from_list(self): + """listing a book""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + booklist = models.List.objects.create( + name="test list", + user=self.local_user, + ) + listitem = models.ListItem.objects.create( + user=self.local_user, + book=self.book, + book_list=booklist, + order=1, + ) + self.assertEqual(booklist.books.count(), 1) + + activity = { + "id": listitem.remote_id, + "type": "Remove", + "actor": "https://example.com/users/rat", + "object": { + "actor": self.remote_user.remote_id, + "type": "ListItem", + "book": self.book.remote_id, + "id": listitem.remote_id, + }, + "target": booklist.remote_id, + "@context": "https://www.w3.org/ns/activitystreams", + } + views.inbox.activity_task(activity) + + self.assertEqual(booklist.books.count(), 0) diff --git a/bookwyrm/tests/views/inbox/test_inbox_update.py b/bookwyrm/tests/views/inbox/test_inbox_update.py new file mode 100644 index 00000000..9fdc9792 --- /dev/null +++ b/bookwyrm/tests/views/inbox/test_inbox_update.py @@ -0,0 +1,171 @@ +""" tests incoming activities""" +import json +import pathlib +from unittest.mock import patch + +from django.test import TestCase + +from bookwyrm import models, views + + +# pylint: disable=too-many-public-methods +class InboxUpdate(TestCase): + """inbox tests""" + + def setUp(self): + """basic user and book data""" + self.local_user = models.User.objects.create_user( + "mouse@example.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + ) + self.local_user.remote_id = "https://example.com/user/mouse" + self.local_user.save(broadcast=False) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + + self.create_json = { + "id": "hi", + "type": "Create", + "actor": "hi", + "to": ["https://www.w3.org/ns/activitystreams#public"], + "cc": ["https://example.com/user/mouse/followers"], + "object": {}, + } + models.SiteSettings.objects.create() + + def test_update_list(self): + """a new list""" + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + book_list = models.List.objects.create( + name="hi", remote_id="https://example.com/list/22", user=self.local_user + ) + activity = { + "type": "Update", + "to": [], + "cc": [], + "actor": "hi", + "id": "sdkjf", + "object": { + "id": "https://example.com/list/22", + "type": "BookList", + "totalItems": 1, + "first": "https://example.com/list/22?page=1", + "last": "https://example.com/list/22?page=1", + "name": "Test List", + "owner": "https://example.com/user/mouse", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "cc": ["https://example.com/user/mouse/followers"], + "summary": "summary text", + "curation": "curated", + "@context": "https://www.w3.org/ns/activitystreams", + }, + } + views.inbox.activity_task(activity) + book_list.refresh_from_db() + self.assertEqual(book_list.name, "Test List") + self.assertEqual(book_list.curation, "curated") + self.assertEqual(book_list.description, "summary text") + self.assertEqual(book_list.remote_id, "https://example.com/list/22") + + def test_update_user(self): + """update an existing user""" + models.UserFollows.objects.create( + user_subject=self.local_user, + user_object=self.remote_user, + ) + models.UserFollows.objects.create( + user_subject=self.remote_user, + user_object=self.local_user, + ) + self.assertTrue(self.remote_user in self.local_user.followers.all()) + self.assertTrue(self.local_user in self.remote_user.followers.all()) + + datafile = pathlib.Path(__file__).parent.joinpath("../../data/ap_user_rat.json") + userdata = json.loads(datafile.read_bytes()) + del userdata["icon"] + self.assertIsNone(self.remote_user.name) + self.assertFalse(self.remote_user.discoverable) + views.inbox.activity_task( + { + "type": "Update", + "to": [], + "cc": [], + "actor": "hi", + "id": "sdkjf", + "object": userdata, + } + ) + user = models.User.objects.get(id=self.remote_user.id) + self.assertEqual(user.name, "RAT???") + self.assertEqual(user.username, "rat@example.com") + self.assertTrue(user.discoverable) + + # make sure relationships aren't disrupted + self.assertTrue(self.remote_user in self.local_user.followers.all()) + self.assertTrue(self.local_user in self.remote_user.followers.all()) + + def test_update_edition(self): + """update an existing edition""" + datafile = pathlib.Path(__file__).parent.joinpath("../../data/bw_edition.json") + bookdata = json.loads(datafile.read_bytes()) + + models.Work.objects.create( + title="Test Work", remote_id="https://bookwyrm.social/book/5988" + ) + book = models.Edition.objects.create( + title="Test Book", remote_id="https://bookwyrm.social/book/5989" + ) + + del bookdata["authors"] + self.assertEqual(book.title, "Test Book") + + with patch("bookwyrm.activitypub.base_activity.set_related_field.delay"): + views.inbox.activity_task( + { + "type": "Update", + "to": [], + "cc": [], + "actor": "hi", + "id": "sdkjf", + "object": bookdata, + } + ) + book = models.Edition.objects.get(id=book.id) + self.assertEqual(book.title, "Piranesi") + self.assertEqual(book.last_edited_by, self.remote_user) + + def test_update_work(self): + """update an existing edition""" + datafile = pathlib.Path(__file__).parent.joinpath("../../data/bw_work.json") + bookdata = json.loads(datafile.read_bytes()) + + book = models.Work.objects.create( + title="Test Book", remote_id="https://bookwyrm.social/book/5988" + ) + + del bookdata["authors"] + self.assertEqual(book.title, "Test Book") + with patch("bookwyrm.activitypub.base_activity.set_related_field.delay"): + views.inbox.activity_task( + { + "type": "Update", + "to": [], + "cc": [], + "actor": "hi", + "id": "sdkjf", + "object": bookdata, + } + ) + book = models.Work.objects.get(id=book.id) + self.assertEqual(book.title, "Piranesi") diff --git a/bookwyrm/tests/views/test_authentication.py b/bookwyrm/tests/views/test_authentication.py index f6d31861..c310b0a2 100644 --- a/bookwyrm/tests/views/test_authentication.py +++ b/bookwyrm/tests/views/test_authentication.py @@ -14,10 +14,10 @@ from bookwyrm.settings import DOMAIN # pylint: disable=too-many-public-methods class AuthenticationViews(TestCase): - """ login and password management """ + """login and password management""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -31,7 +31,7 @@ class AuthenticationViews(TestCase): self.settings = models.SiteSettings.objects.create(id=1) def test_login_get(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" login = views.Login.as_view() request = self.factory.get("") request.user = self.anonymous_user @@ -47,7 +47,7 @@ class AuthenticationViews(TestCase): self.assertEqual(result.status_code, 302) def test_register(self): - """ create a user """ + """create a user""" view = views.Register.as_view() self.assertEqual(models.User.objects.count(), 1) request = self.factory.post( @@ -68,7 +68,7 @@ class AuthenticationViews(TestCase): self.assertEqual(nutria.local, True) def test_register_trailing_space(self): - """ django handles this so weirdly """ + """django handles this so weirdly""" view = views.Register.as_view() request = self.factory.post( "register/", @@ -84,7 +84,7 @@ class AuthenticationViews(TestCase): self.assertEqual(nutria.local, True) def test_register_invalid_email(self): - """ gotta have an email """ + """gotta have an email""" view = views.Register.as_view() self.assertEqual(models.User.objects.count(), 1) request = self.factory.post( @@ -95,7 +95,7 @@ class AuthenticationViews(TestCase): response.render() def test_register_invalid_username(self): - """ gotta have an email """ + """gotta have an email""" view = views.Register.as_view() self.assertEqual(models.User.objects.count(), 1) request = self.factory.post( @@ -123,7 +123,7 @@ class AuthenticationViews(TestCase): response.render() def test_register_closed_instance(self): - """ you can't just register """ + """you can't just register""" view = views.Register.as_view() self.settings.allow_registration = False self.settings.save() @@ -135,7 +135,7 @@ class AuthenticationViews(TestCase): view(request) def test_register_invite(self): - """ you can't just register """ + """you can't just register""" view = views.Register.as_view() self.settings.allow_registration = False self.settings.save() diff --git a/bookwyrm/tests/views/test_author.py b/bookwyrm/tests/views/test_author.py index bb047b7c..5dfbc350 100644 --- a/bookwyrm/tests/views/test_author.py +++ b/bookwyrm/tests/views/test_author.py @@ -12,10 +12,10 @@ from bookwyrm.activitypub import ActivitypubResponse class AuthorViews(TestCase): - """ author views""" + """author views""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -42,7 +42,7 @@ class AuthorViews(TestCase): models.SiteSettings.objects.create() def test_author_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Author.as_view() author = models.Author.objects.create(name="Jessica") request = self.factory.get("") @@ -62,7 +62,7 @@ class AuthorViews(TestCase): self.assertEqual(result.status_code, 200) def test_edit_author_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.EditAuthor.as_view() author = models.Author.objects.create(name="Test Author") request = self.factory.get("") @@ -76,7 +76,7 @@ class AuthorViews(TestCase): self.assertEqual(result.status_code, 200) def test_edit_author(self): - """ edit an author """ + """edit an author""" view = views.EditAuthor.as_view() author = models.Author.objects.create(name="Test Author") self.local_user.groups.add(self.group) @@ -93,7 +93,7 @@ class AuthorViews(TestCase): self.assertEqual(author.last_edited_by, self.local_user) def test_edit_author_non_editor(self): - """ edit an author with invalid post data""" + """edit an author with invalid post data""" view = views.EditAuthor.as_view() author = models.Author.objects.create(name="Test Author") form = forms.AuthorForm(instance=author) @@ -108,7 +108,7 @@ class AuthorViews(TestCase): self.assertEqual(author.name, "Test Author") def test_edit_author_invalid_form(self): - """ edit an author with invalid post data""" + """edit an author with invalid post data""" view = views.EditAuthor.as_view() author = models.Author.objects.create(name="Test Author") self.local_user.groups.add(self.group) diff --git a/bookwyrm/tests/views/test_block.py b/bookwyrm/tests/views/test_block.py index 71583d70..0b754689 100644 --- a/bookwyrm/tests/views/test_block.py +++ b/bookwyrm/tests/views/test_block.py @@ -9,10 +9,10 @@ from bookwyrm import models, views @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class BlockViews(TestCase): - """ view user and edit profile """ + """view user and edit profile""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -34,7 +34,7 @@ class BlockViews(TestCase): models.SiteSettings.objects.create() def test_block_get(self, _): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Block.as_view() request = self.factory.get("") request.user = self.local_user @@ -44,7 +44,7 @@ class BlockViews(TestCase): self.assertEqual(result.status_code, 200) def test_block_post(self, _): - """ create a "block" database entry from an activity """ + """create a "block" database entry from an activity""" view = views.Block.as_view() self.local_user.followers.add(self.remote_user) models.UserFollowRequest.objects.create( @@ -65,7 +65,7 @@ class BlockViews(TestCase): self.assertFalse(models.UserFollowRequest.objects.exists()) def test_unblock(self, _): - """ undo a block """ + """undo a block""" self.local_user.blocks.add(self.remote_user) request = self.factory.post("") request.user = self.local_user diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index ade6131d..dce50868 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -18,10 +18,10 @@ from bookwyrm.activitypub import ActivitypubResponse class BookViews(TestCase): - """ books books books """ + """books books books""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -47,8 +47,41 @@ class BookViews(TestCase): ) models.SiteSettings.objects.create() + def test_date_regression(self): + """ensure that creating a new book actually saves the published date fields + + this was initially a regression due to using a custom date picker tag + """ + first_published_date = "2021-04-20" + published_date = "2022-04-20" + self.local_user.groups.add(self.group) + view = views.EditBook.as_view() + form = forms.EditionForm( + { + "title": "New Title", + "last_edited_by": self.local_user.id, + "first_published_date": first_published_date, + "published_date": published_date, + } + ) + request = self.factory.post("", form.data) + request.user = self.local_user + + with patch("bookwyrm.connectors.connector_manager.local_search"): + result = view(request) + result.render() + + self.assertContains( + result, + f'', + ) + self.assertContains( + result, + f'', + ) + def test_book_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Book.as_view() request = self.factory.get("") request.user = self.local_user @@ -67,7 +100,7 @@ class BookViews(TestCase): self.assertEqual(result.status_code, 200) def test_edit_book_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.EditBook.as_view() request = self.factory.get("") request.user = self.local_user @@ -78,7 +111,7 @@ class BookViews(TestCase): self.assertEqual(result.status_code, 200) def test_edit_book(self): - """ lets a user edit a book """ + """lets a user edit a book""" view = views.EditBook.as_view() self.local_user.groups.add(self.group) form = forms.EditionForm(instance=self.book) @@ -92,7 +125,7 @@ class BookViews(TestCase): self.assertEqual(self.book.title, "New Title") def test_edit_book_add_author(self): - """ lets a user edit a book with new authors """ + """lets a user edit a book with new authors""" view = views.EditBook.as_view() self.local_user.groups.add(self.group) form = forms.EditionForm(instance=self.book) @@ -110,7 +143,7 @@ class BookViews(TestCase): self.assertEqual(self.book.title, "Example Edition") def test_edit_book_add_new_author_confirm(self): - """ lets a user edit a book confirmed with new authors """ + """lets a user edit a book confirmed with new authors""" view = views.ConfirmEditBook.as_view() self.local_user.groups.add(self.group) form = forms.EditionForm(instance=self.book) @@ -129,7 +162,7 @@ class BookViews(TestCase): self.assertEqual(self.book.authors.first().name, "Sappho") def test_edit_book_remove_author(self): - """ remove an author from a book """ + """remove an author from a book""" author = models.Author.objects.create(name="Sappho") self.book.authors.add(author) form = forms.EditionForm(instance=self.book) @@ -149,7 +182,7 @@ class BookViews(TestCase): self.assertFalse(self.book.authors.exists()) def test_create_book(self): - """ create an entirely new book and work """ + """create an entirely new book and work""" view = views.ConfirmEditBook.as_view() self.local_user.groups.add(self.group) form = forms.EditionForm() @@ -163,7 +196,7 @@ class BookViews(TestCase): self.assertEqual(book.parent_work.title, "New Title") def test_create_book_existing_work(self): - """ create an entirely new book and work """ + """create an entirely new book and work""" view = views.ConfirmEditBook.as_view() self.local_user.groups.add(self.group) form = forms.EditionForm() @@ -178,7 +211,7 @@ class BookViews(TestCase): self.assertEqual(book.parent_work, self.work) def test_create_book_with_author(self): - """ create an entirely new book and work """ + """create an entirely new book and work""" view = views.ConfirmEditBook.as_view() self.local_user.groups.add(self.group) form = forms.EditionForm() @@ -196,7 +229,7 @@ class BookViews(TestCase): self.assertEqual(book.authors.first(), book.parent_work.authors.first()) def test_switch_edition(self): - """ updates user's relationships to a book """ + """updates user's relationships to a book""" work = models.Work.objects.create(title="test work") edition1 = models.Edition.objects.create(title="first ed", parent_work=work) edition2 = models.Edition.objects.create(title="second ed", parent_work=work) @@ -220,7 +253,7 @@ class BookViews(TestCase): self.assertEqual(models.ReadThrough.objects.get().book, edition2) def test_editions_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Editions.as_view() request = self.factory.get("") with patch("bookwyrm.views.books.is_api_request") as is_api: @@ -238,7 +271,7 @@ class BookViews(TestCase): self.assertEqual(result.status_code, 200) def test_upload_cover_file(self): - """ add a cover via file upload """ + """add a cover via file upload""" self.assertFalse(self.book.cover) image_file = pathlib.Path(__file__).parent.joinpath( "../../static/images/default_avi.jpg" @@ -263,7 +296,7 @@ class BookViews(TestCase): @responses.activate def test_upload_cover_url(self): - """ add a cover via url """ + """add a cover via url""" self.assertFalse(self.book.cover) image_file = pathlib.Path(__file__).parent.joinpath( "../../static/images/default_avi.jpg" diff --git a/bookwyrm/tests/views/test_directory.py b/bookwyrm/tests/views/test_directory.py index 80d9eaf7..cada50bc 100644 --- a/bookwyrm/tests/views/test_directory.py +++ b/bookwyrm/tests/views/test_directory.py @@ -7,10 +7,10 @@ from bookwyrm import models, views # pylint: disable=unused-argument class DirectoryViews(TestCase): - """ tag views""" + """tag views""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -32,7 +32,7 @@ class DirectoryViews(TestCase): models.SiteSettings.objects.create() def test_directory_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Directory.as_view() request = self.factory.get("") request.user = self.local_user diff --git a/bookwyrm/tests/views/test_federation.py b/bookwyrm/tests/views/test_federation.py index a60ea432..f17f7624 100644 --- a/bookwyrm/tests/views/test_federation.py +++ b/bookwyrm/tests/views/test_federation.py @@ -1,16 +1,19 @@ """ test for app action functionality """ +import json +from unittest.mock import patch +from django.core.files.uploadedfile import SimpleUploadedFile from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory -from bookwyrm import models, views +from bookwyrm import forms, models, views class FederationViews(TestCase): - """ every response to a get request, html or json """ + """every response to a get request, html or json""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -19,10 +22,20 @@ class FederationViews(TestCase): local=True, localname="mouse", ) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) models.SiteSettings.objects.create() def test_federation_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Federation.as_view() request = self.factory.get("") request.user = self.local_user @@ -33,7 +46,7 @@ class FederationViews(TestCase): self.assertEqual(result.status_code, 200) def test_server_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" server = models.FederatedServer.objects.create(server_name="hi.there.com") view = views.FederatedServer.as_view() request = self.factory.get("") @@ -44,3 +57,111 @@ class FederationViews(TestCase): self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) + + def test_server_page_block(self): + """block a server""" + server = models.FederatedServer.objects.create(server_name="hi.there.com") + self.remote_user.federated_server = server + self.remote_user.save() + + self.assertEqual(server.status, "federated") + + view = views.federation.block_server + request = self.factory.post("") + request.user = self.local_user + request.user.is_superuser = True + + view(request, server.id) + server.refresh_from_db() + self.remote_user.refresh_from_db() + self.assertEqual(server.status, "blocked") + # and the user was deactivated + self.assertFalse(self.remote_user.is_active) + + def test_server_page_unblock(self): + """unblock a server""" + server = models.FederatedServer.objects.create( + server_name="hi.there.com", status="blocked" + ) + self.remote_user.federated_server = server + self.remote_user.is_active = False + self.remote_user.deactivation_reason = "domain_block" + self.remote_user.save() + + request = self.factory.post("") + request.user = self.local_user + request.user.is_superuser = True + + views.federation.unblock_server(request, server.id) + server.refresh_from_db() + self.remote_user.refresh_from_db() + self.assertEqual(server.status, "federated") + # and the user was re-activated + self.assertTrue(self.remote_user.is_active) + + def test_add_view_get(self): + """there are so many views, this just makes sure it LOADS""" + # create mode + view = views.AddFederatedServer.as_view() + request = self.factory.get("") + request.user = self.local_user + request.user.is_superuser = True + + result = view(request) + self.assertIsInstance(result, TemplateResponse) + result.render() + self.assertEqual(result.status_code, 200) + + def test_add_view_post_create(self): + """create a server entry""" + form = forms.ServerForm() + form.data["server_name"] = "remote.server" + form.data["application_type"] = "coolsoft" + form.data["status"] = "blocked" + + view = views.AddFederatedServer.as_view() + request = self.factory.post("", form.data) + request.user = self.local_user + request.user.is_superuser = True + + view(request) + server = models.FederatedServer.objects.get() + self.assertEqual(server.server_name, "remote.server") + self.assertEqual(server.application_type, "coolsoft") + self.assertEqual(server.status, "blocked") + + def test_import_blocklist(self): + """load a json file with a list of servers to block""" + server = models.FederatedServer.objects.create(server_name="hi.there.com") + self.remote_user.federated_server = server + self.remote_user.save() + + data = [ + {"instance": "server.name", "url": "https://explanation.url"}, # new server + {"instance": "hi.there.com", "url": "https://explanation.url"}, # existing + {"a": "b"}, # invalid + ] + json.dump(data, open("file.json", "w")) + + view = views.ImportServerBlocklist.as_view() + request = self.factory.post( + "", + { + "json_file": SimpleUploadedFile( + "file.json", open("file.json", "rb").read() + ) + }, + ) + request.user = self.local_user + request.user.is_superuser = True + + view(request) + server.refresh_from_db() + self.remote_user.refresh_from_db() + + self.assertEqual(models.FederatedServer.objects.count(), 2) + self.assertEqual(server.status, "blocked") + self.assertFalse(self.remote_user.is_active) + created = models.FederatedServer.objects.get(server_name="server.name") + self.assertEqual(created.status, "blocked") + self.assertEqual(created.notes, "https://explanation.url") diff --git a/bookwyrm/tests/views/test_feed.py b/bookwyrm/tests/views/test_feed.py index dd38a3eb..a6a3d967 100644 --- a/bookwyrm/tests/views/test_feed.py +++ b/bookwyrm/tests/views/test_feed.py @@ -17,10 +17,10 @@ from bookwyrm.activitypub import ActivitypubResponse @patch("bookwyrm.activitystreams.ActivityStream.get_activity_stream") @patch("bookwyrm.activitystreams.ActivityStream.add_status") class FeedViews(TestCase): - """ activity feed, statuses, dms """ + """activity feed, statuses, dms""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -37,7 +37,7 @@ class FeedViews(TestCase): models.SiteSettings.objects.create() def test_feed(self, *_): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Feed.as_view() request = self.factory.get("") request.user = self.local_user @@ -47,7 +47,7 @@ class FeedViews(TestCase): self.assertEqual(result.status_code, 200) def test_status_page(self, *_): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Status.as_view() with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create(content="hi", user=self.local_user) @@ -67,7 +67,7 @@ class FeedViews(TestCase): self.assertEqual(result.status_code, 200) def test_status_page_not_found(self, *_): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Status.as_view() request = self.factory.get("") @@ -79,7 +79,7 @@ class FeedViews(TestCase): self.assertEqual(result.status_code, 404) def test_status_page_with_image(self, *_): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Status.as_view() image_file = pathlib.Path(__file__).parent.joinpath( @@ -115,7 +115,7 @@ class FeedViews(TestCase): self.assertEqual(result.status_code, 200) def test_replies_page(self, *_): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Replies.as_view() with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create(content="hi", user=self.local_user) @@ -135,7 +135,7 @@ class FeedViews(TestCase): self.assertEqual(result.status_code, 200) def test_direct_messages_page(self, *_): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.DirectMessage.as_view() request = self.factory.get("") request.user = self.local_user @@ -145,7 +145,7 @@ class FeedViews(TestCase): self.assertEqual(result.status_code, 200) def test_get_suggested_book(self, *_): - """ gets books the ~*~ algorithm ~*~ thinks you want to post about """ + """gets books the ~*~ algorithm ~*~ thinks you want to post about""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.ShelfBook.objects.create( book=self.book, diff --git a/bookwyrm/tests/views/test_follow.py b/bookwyrm/tests/views/test_follow.py index 6b4de05d..45e60d3c 100644 --- a/bookwyrm/tests/views/test_follow.py +++ b/bookwyrm/tests/views/test_follow.py @@ -11,10 +11,10 @@ from bookwyrm import models, views class BookViews(TestCase): - """ books books books """ + """books books books""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -50,7 +50,7 @@ class BookViews(TestCase): ) def test_handle_follow_remote(self): - """ send a follow request """ + """send a follow request""" request = self.factory.post("", {"user": self.remote_user.username}) request.user = self.local_user self.assertEqual(models.UserFollowRequest.objects.count(), 0) @@ -65,7 +65,7 @@ class BookViews(TestCase): self.assertEqual(rel.status, "follow_request") def test_handle_follow_local_manually_approves(self): - """ send a follow request """ + """send a follow request""" rat = models.User.objects.create_user( "rat@local.com", "rat@rat.com", @@ -88,7 +88,7 @@ class BookViews(TestCase): self.assertEqual(rel.status, "follow_request") def test_handle_follow_local(self): - """ send a follow request """ + """send a follow request""" rat = models.User.objects.create_user( "rat@local.com", "rat@rat.com", @@ -111,7 +111,7 @@ class BookViews(TestCase): self.assertEqual(rel.status, "follows") def test_handle_unfollow(self): - """ send an unfollow """ + """send an unfollow""" request = self.factory.post("", {"user": self.remote_user.username}) request.user = self.local_user self.remote_user.followers.add(self.local_user) @@ -125,7 +125,7 @@ class BookViews(TestCase): self.assertEqual(self.remote_user.followers.count(), 0) def test_handle_accept(self): - """ accept a follow request """ + """accept a follow request""" self.local_user.manually_approves_followers = True self.local_user.save(broadcast=False) request = self.factory.post("", {"user": self.remote_user.username}) @@ -142,7 +142,7 @@ class BookViews(TestCase): self.assertEqual(self.local_user.followers.first(), self.remote_user) def test_handle_reject(self): - """ reject a follow request """ + """reject a follow request""" self.local_user.manually_approves_followers = True self.local_user.save(broadcast=False) request = self.factory.post("", {"user": self.remote_user.username}) diff --git a/bookwyrm/tests/views/test_get_started.py b/bookwyrm/tests/views/test_get_started.py index 71eb4060..1c55da08 100644 --- a/bookwyrm/tests/views/test_get_started.py +++ b/bookwyrm/tests/views/test_get_started.py @@ -8,10 +8,10 @@ from bookwyrm import forms, models, views class GetStartedViews(TestCase): - """ helping new users get oriented """ + """helping new users get oriented""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -31,7 +31,7 @@ class GetStartedViews(TestCase): models.SiteSettings.objects.create() def test_profile_view(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.GetStartedProfile.as_view() request = self.factory.get("") request.user = self.local_user @@ -43,7 +43,7 @@ class GetStartedViews(TestCase): self.assertEqual(result.status_code, 200) def test_profile_view_post(self): - """ save basic user details """ + """save basic user details""" view = views.GetStartedProfile.as_view() form = forms.LimitedEditUserForm(instance=self.local_user) form.data["name"] = "New Name" @@ -61,7 +61,7 @@ class GetStartedViews(TestCase): self.assertTrue(self.local_user.discoverable) def test_books_view(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.GetStartedBooks.as_view() request = self.factory.get("") request.user = self.local_user @@ -73,7 +73,7 @@ class GetStartedViews(TestCase): self.assertEqual(result.status_code, 200) def test_books_view_with_query(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.GetStartedBooks.as_view() request = self.factory.get("?query=Example") request.user = self.local_user @@ -85,7 +85,7 @@ class GetStartedViews(TestCase): self.assertEqual(result.status_code, 200) def test_books_view_post(self): - """ shelve some books """ + """shelve some books""" view = views.GetStartedBooks.as_view() data = {self.book.id: self.local_user.shelf_set.first().id} request = self.factory.post("", data) @@ -103,7 +103,7 @@ class GetStartedViews(TestCase): self.assertEqual(shelfbook.user, self.local_user) def test_users_view(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.GetStartedUsers.as_view() request = self.factory.get("") request.user = self.local_user @@ -115,7 +115,7 @@ class GetStartedViews(TestCase): self.assertEqual(result.status_code, 200) def test_users_view_with_query(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.GetStartedUsers.as_view() request = self.factory.get("?query=rat") request.user = self.local_user diff --git a/bookwyrm/tests/views/test_goal.py b/bookwyrm/tests/views/test_goal.py index cbe4fe01..4e8f6ee2 100644 --- a/bookwyrm/tests/views/test_goal.py +++ b/bookwyrm/tests/views/test_goal.py @@ -11,10 +11,10 @@ from bookwyrm import models, views class GoalViews(TestCase): - """ viewing and creating statuses """ + """viewing and creating statuses""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -41,7 +41,7 @@ class GoalViews(TestCase): models.SiteSettings.objects.create() def test_goal_page_no_goal(self): - """ view a reading goal page for another's unset goal """ + """view a reading goal page for another's unset goal""" view = views.Goal.as_view() request = self.factory.get("") request.user = self.rat @@ -50,7 +50,7 @@ class GoalViews(TestCase): self.assertEqual(result.status_code, 404) def test_goal_page_no_goal_self(self): - """ view a reading goal page for your own unset goal """ + """view a reading goal page for your own unset goal""" view = views.Goal.as_view() request = self.factory.get("") request.user = self.local_user @@ -60,7 +60,7 @@ class GoalViews(TestCase): self.assertIsInstance(result, TemplateResponse) def test_goal_page_anonymous(self): - """ can't view it without login """ + """can't view it without login""" view = views.Goal.as_view() request = self.factory.get("") request.user = self.anonymous_user @@ -69,7 +69,7 @@ class GoalViews(TestCase): self.assertEqual(result.status_code, 302) def test_goal_page_public(self): - """ view a user's public goal """ + """view a user's public goal""" models.ReadThrough.objects.create( finish_date=timezone.now(), user=self.local_user, @@ -91,7 +91,7 @@ class GoalViews(TestCase): self.assertIsInstance(result, TemplateResponse) def test_goal_page_private(self): - """ view a user's private goal """ + """view a user's private goal""" models.AnnualGoal.objects.create( user=self.local_user, year=2020, goal=15, privacy="followers" ) @@ -104,7 +104,7 @@ class GoalViews(TestCase): @patch("bookwyrm.activitystreams.ActivityStream.add_status") def test_create_goal(self, _): - """ create a new goal """ + """create a new goal""" view = views.Goal.as_view() request = self.factory.post( "", diff --git a/bookwyrm/tests/views/test_helpers.py b/bookwyrm/tests/views/test_helpers.py index c646b4b4..e2e041e9 100644 --- a/bookwyrm/tests/views/test_helpers.py +++ b/bookwyrm/tests/views/test_helpers.py @@ -13,10 +13,10 @@ from bookwyrm.settings import USER_AGENT @patch("bookwyrm.activitystreams.ActivityStream.add_status") class ViewsHelpers(TestCase): - """ viewing and creating statuses """ + """viewing and creating statuses""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -53,12 +53,12 @@ class ViewsHelpers(TestCase): ) def test_get_edition(self, _): - """ given an edition or a work, returns an edition """ + """given an edition or a work, returns an edition""" self.assertEqual(views.helpers.get_edition(self.book.id), self.book) self.assertEqual(views.helpers.get_edition(self.work.id), self.book) def test_get_user_from_username(self, _): - """ works for either localname or username """ + """works for either localname or username""" self.assertEqual( views.helpers.get_user_from_username(self.local_user, "mouse"), self.local_user, @@ -71,7 +71,7 @@ class ViewsHelpers(TestCase): views.helpers.get_user_from_username(self.local_user, "mojfse@example.com") def test_is_api_request(self, _): - """ should it return html or json """ + """should it return html or json""" request = self.factory.get("/path") request.headers = {"Accept": "application/json"} self.assertTrue(views.helpers.is_api_request(request)) @@ -85,12 +85,12 @@ class ViewsHelpers(TestCase): self.assertFalse(views.helpers.is_api_request(request)) def test_is_api_request_no_headers(self, _): - """ should it return html or json """ + """should it return html or json""" request = self.factory.get("/path") self.assertFalse(views.helpers.is_api_request(request)) def test_is_bookwyrm_request(self, _): - """ checks if a request came from a bookwyrm instance """ + """checks if a request came from a bookwyrm instance""" request = self.factory.get("", {"q": "Test Book"}) self.assertFalse(views.helpers.is_bookwyrm_request(request)) @@ -105,16 +105,19 @@ class ViewsHelpers(TestCase): self.assertTrue(views.helpers.is_bookwyrm_request(request)) def test_existing_user(self, _): - """ simple database lookup by username """ + """simple database lookup by username""" result = views.helpers.handle_remote_webfinger("@mouse@local.com") self.assertEqual(result, self.local_user) result = views.helpers.handle_remote_webfinger("mouse@local.com") self.assertEqual(result, self.local_user) + result = views.helpers.handle_remote_webfinger("mOuSe@loCal.cOm") + self.assertEqual(result, self.local_user) + @responses.activate def test_load_user(self, _): - """ find a remote user using webfinger """ + """find a remote user using webfinger""" username = "mouse@example.com" wellknown = { "subject": "acct:mouse@example.com", @@ -143,8 +146,17 @@ class ViewsHelpers(TestCase): self.assertIsInstance(result, models.User) self.assertEqual(result.username, "mouse@example.com") + def test_user_on_blocked_server(self, _): + """find a remote user using webfinger""" + models.FederatedServer.objects.create( + server_name="example.com", status="blocked" + ) + + result = views.helpers.handle_remote_webfinger("@mouse@example.com") + self.assertIsNone(result) + def test_handle_reading_status_to_read(self, _): - """ posts shelve activities """ + """posts shelve activities""" shelf = self.local_user.shelf_set.get(identifier="to-read") with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.helpers.handle_reading_status( @@ -156,7 +168,7 @@ class ViewsHelpers(TestCase): self.assertEqual(status.content, "wants to read") def test_handle_reading_status_reading(self, _): - """ posts shelve activities """ + """posts shelve activities""" shelf = self.local_user.shelf_set.get(identifier="reading") with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.helpers.handle_reading_status( @@ -168,7 +180,7 @@ class ViewsHelpers(TestCase): self.assertEqual(status.content, "started reading") def test_handle_reading_status_read(self, _): - """ posts shelve activities """ + """posts shelve activities""" shelf = self.local_user.shelf_set.get(identifier="read") with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.helpers.handle_reading_status( @@ -180,75 +192,15 @@ class ViewsHelpers(TestCase): self.assertEqual(status.content, "finished reading") def test_handle_reading_status_other(self, _): - """ posts shelve activities """ + """posts shelve activities""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.helpers.handle_reading_status( self.local_user, self.shelf, self.book, "public" ) self.assertFalse(models.GeneratedNote.objects.exists()) - def test_object_visible_to_user(self, _): - """ does a user have permission to view an object """ - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="public" - ) - self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj)) - - obj = models.Shelf.objects.create( - name="test", user=self.remote_user, privacy="unlisted" - ) - self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj)) - - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="followers" - ) - self.assertFalse(views.helpers.object_visible_to_user(self.local_user, obj)) - - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="direct" - ) - self.assertFalse(views.helpers.object_visible_to_user(self.local_user, obj)) - - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="direct" - ) - obj.mention_users.add(self.local_user) - self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj)) - - def test_object_visible_to_user_follower(self, _): - """ what you can see if you follow a user """ - self.remote_user.followers.add(self.local_user) - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="followers" - ) - self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj)) - - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="direct" - ) - self.assertFalse(views.helpers.object_visible_to_user(self.local_user, obj)) - - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="direct" - ) - obj.mention_users.add(self.local_user) - self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj)) - - def test_object_visible_to_user_blocked(self, _): - """ you can't see it if they block you """ - self.remote_user.blocks.add(self.local_user) - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="public" - ) - self.assertFalse(views.helpers.object_visible_to_user(self.local_user, obj)) - - obj = models.Shelf.objects.create( - name="test", user=self.remote_user, privacy="unlisted" - ) - self.assertFalse(views.helpers.object_visible_to_user(self.local_user, obj)) - def test_get_annotated_users(self, _): - """ list of people you might know """ + """list of people you might know""" user_1 = models.User.objects.create_user( "nutria@local.com", "nutria@nutria.com", @@ -295,7 +247,7 @@ class ViewsHelpers(TestCase): self.assertEqual(remote_user_annotated.shared_books, 0) def test_get_annotated_users_counts(self, _): - """ correct counting for multiple shared attributed """ + """correct counting for multiple shared attributed""" user_1 = models.User.objects.create_user( "nutria@local.com", "nutria@nutria.com", diff --git a/bookwyrm/tests/views/test_import.py b/bookwyrm/tests/views/test_import.py index 4de2cfb9..22694623 100644 --- a/bookwyrm/tests/views/test_import.py +++ b/bookwyrm/tests/views/test_import.py @@ -9,10 +9,10 @@ from bookwyrm import views class ImportViews(TestCase): - """ goodreads import views """ + """goodreads import views""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -24,7 +24,7 @@ class ImportViews(TestCase): models.SiteSettings.objects.create() def test_import_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Import.as_view() request = self.factory.get("") request.user = self.local_user @@ -34,7 +34,7 @@ class ImportViews(TestCase): self.assertEqual(result.status_code, 200) def test_import_status(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.ImportStatus.as_view() import_job = models.ImportJob.objects.create(user=self.local_user) request = self.factory.get("") @@ -47,7 +47,7 @@ class ImportViews(TestCase): self.assertEqual(result.status_code, 200) def test_retry_import(self): - """ retry failed items """ + """retry failed items""" view = views.ImportStatus.as_view() import_job = models.ImportJob.objects.create( user=self.local_user, privacy="unlisted" diff --git a/bookwyrm/tests/views/test_inbox.py b/bookwyrm/tests/views/test_inbox.py deleted file mode 100644 index f44a79c6..00000000 --- a/bookwyrm/tests/views/test_inbox.py +++ /dev/null @@ -1,988 +0,0 @@ -""" tests incoming activities""" -from datetime import datetime -import json -import pathlib -from unittest.mock import patch - -from django.http import HttpResponseNotAllowed, HttpResponseNotFound -from django.test import TestCase, Client -import responses - -from bookwyrm import models, views - - -# pylint: disable=too-many-public-methods -class Inbox(TestCase): - """ readthrough tests """ - - def setUp(self): - """ basic user and book data """ - self.client = Client() - self.local_user = models.User.objects.create_user( - "mouse@example.com", - "mouse@mouse.com", - "mouseword", - local=True, - localname="mouse", - ) - self.local_user.remote_id = "https://example.com/user/mouse" - self.local_user.save(broadcast=False) - with patch("bookwyrm.models.user.set_remote_server.delay"): - self.remote_user = models.User.objects.create_user( - "rat", - "rat@rat.com", - "ratword", - local=False, - remote_id="https://example.com/users/rat", - inbox="https://example.com/users/rat/inbox", - outbox="https://example.com/users/rat/outbox", - ) - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - with patch("bookwyrm.activitystreams.ActivityStream.add_status"): - self.status = models.Status.objects.create( - user=self.local_user, - content="Test status", - remote_id="https://example.com/status/1", - ) - - self.create_json = { - "id": "hi", - "type": "Create", - "actor": "hi", - "to": ["https://www.w3.org/ns/activitystreams#public"], - "cc": ["https://example.com/user/mouse/followers"], - "object": {}, - } - models.SiteSettings.objects.create() - - def test_inbox_invalid_get(self): - """ shouldn't try to handle if the user is not found """ - result = self.client.get("/inbox", content_type="application/json") - self.assertIsInstance(result, HttpResponseNotAllowed) - - def test_inbox_invalid_user(self): - """ shouldn't try to handle if the user is not found """ - result = self.client.post( - "/user/bleh/inbox", - '{"type": "Test", "object": "exists"}', - content_type="application/json", - ) - self.assertIsInstance(result, HttpResponseNotFound) - - def test_inbox_invalid_bad_signature(self): - """ bad request for invalid signature """ - with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: - mock_valid.return_value = False - result = self.client.post( - "/user/mouse/inbox", - '{"type": "Announce", "object": "exists"}', - content_type="application/json", - ) - self.assertEqual(result.status_code, 401) - - def test_inbox_invalid_bad_signature_delete(self): - """ invalid signature for Delete is okay though """ - with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: - mock_valid.return_value = False - result = self.client.post( - "/user/mouse/inbox", - '{"type": "Delete", "object": "exists"}', - content_type="application/json", - ) - self.assertEqual(result.status_code, 200) - - def test_inbox_unknown_type(self): - """ never heard of that activity type, don't have a handler for it """ - with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: - result = self.client.post( - "/inbox", - '{"type": "Fish", "object": "exists"}', - content_type="application/json", - ) - mock_valid.return_value = True - self.assertIsInstance(result, HttpResponseNotFound) - - def test_inbox_success(self): - """ a known type, for which we start a task """ - activity = self.create_json - activity["object"] = { - "id": "https://example.com/list/22", - "type": "BookList", - "totalItems": 1, - "first": "https://example.com/list/22?page=1", - "last": "https://example.com/list/22?page=1", - "name": "Test List", - "owner": "https://example.com/user/mouse", - "to": ["https://www.w3.org/ns/activitystreams#Public"], - "cc": ["https://example.com/user/mouse/followers"], - "summary": "summary text", - "curation": "curated", - "@context": "https://www.w3.org/ns/activitystreams", - } - with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: - mock_valid.return_value = True - - with patch("bookwyrm.views.inbox.activity_task.delay"): - result = self.client.post( - "/inbox", json.dumps(activity), content_type="application/json" - ) - self.assertEqual(result.status_code, 200) - - def test_handle_create_status(self): - """ the "it justs works" mode """ - self.assertEqual(models.Status.objects.count(), 1) - - datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_quotation.json") - status_data = json.loads(datafile.read_bytes()) - models.Edition.objects.create( - title="Test Book", remote_id="https://example.com/book/1" - ) - activity = self.create_json - activity["object"] = status_data - - with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock: - views.inbox.activity_task(activity) - self.assertTrue(redis_mock.called) - - status = models.Quotation.objects.get() - self.assertEqual( - status.remote_id, "https://example.com/user/mouse/quotation/13" - ) - self.assertEqual(status.quote, "quote body") - self.assertEqual(status.content, "commentary") - self.assertEqual(status.user, self.local_user) - self.assertEqual(models.Status.objects.count(), 2) - - # while we're here, lets ensure we avoid dupes - views.inbox.activity_task(activity) - self.assertEqual(models.Status.objects.count(), 2) - - def test_handle_create_status_remote_note_with_mention(self): - """ should only create it under the right circumstances """ - self.assertEqual(models.Status.objects.count(), 1) - self.assertFalse( - models.Notification.objects.filter(user=self.local_user).exists() - ) - - datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_note.json") - status_data = json.loads(datafile.read_bytes()) - activity = self.create_json - activity["object"] = status_data - - with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock: - views.inbox.activity_task(activity) - self.assertTrue(redis_mock.called) - status = models.Status.objects.last() - self.assertEqual(status.content, "test content in note") - self.assertEqual(status.mention_users.first(), self.local_user) - self.assertTrue( - models.Notification.objects.filter(user=self.local_user).exists() - ) - self.assertEqual(models.Notification.objects.get().notification_type, "MENTION") - - def test_handle_create_status_remote_note_with_reply(self): - """ should only create it under the right circumstances """ - self.assertEqual(models.Status.objects.count(), 1) - self.assertFalse(models.Notification.objects.filter(user=self.local_user)) - - datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_note.json") - status_data = json.loads(datafile.read_bytes()) - del status_data["tag"] - status_data["inReplyTo"] = self.status.remote_id - activity = self.create_json - activity["object"] = status_data - - with patch("bookwyrm.activitystreams.ActivityStream.add_status") as redis_mock: - views.inbox.activity_task(activity) - self.assertTrue(redis_mock.called) - status = models.Status.objects.last() - self.assertEqual(status.content, "test content in note") - self.assertEqual(status.reply_parent, self.status) - self.assertTrue(models.Notification.objects.filter(user=self.local_user)) - self.assertEqual(models.Notification.objects.get().notification_type, "REPLY") - - def test_handle_create_list(self): - """ a new list """ - activity = self.create_json - activity["object"] = { - "id": "https://example.com/list/22", - "type": "BookList", - "totalItems": 1, - "first": "https://example.com/list/22?page=1", - "last": "https://example.com/list/22?page=1", - "name": "Test List", - "owner": "https://example.com/user/mouse", - "to": ["https://www.w3.org/ns/activitystreams#Public"], - "cc": ["https://example.com/user/mouse/followers"], - "summary": "summary text", - "curation": "curated", - "@context": "https://www.w3.org/ns/activitystreams", - } - views.inbox.activity_task(activity) - book_list = models.List.objects.get() - self.assertEqual(book_list.name, "Test List") - self.assertEqual(book_list.curation, "curated") - self.assertEqual(book_list.description, "summary text") - self.assertEqual(book_list.remote_id, "https://example.com/list/22") - - def test_handle_follow(self): - """ remote user wants to follow local user """ - activity = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/users/rat/follows/123", - "type": "Follow", - "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse", - } - - self.assertFalse(models.UserFollowRequest.objects.exists()) - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: - views.inbox.activity_task(activity) - self.assertEqual(mock.call_count, 1) - - # notification created - notification = models.Notification.objects.get() - self.assertEqual(notification.user, self.local_user) - self.assertEqual(notification.notification_type, "FOLLOW") - - # the request should have been deleted - self.assertFalse(models.UserFollowRequest.objects.exists()) - - # the follow relationship should exist - follow = models.UserFollows.objects.get(user_object=self.local_user) - self.assertEqual(follow.user_subject, self.remote_user) - - def test_handle_follow_manually_approved(self): - """ needs approval before following """ - activity = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/users/rat/follows/123", - "type": "Follow", - "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse", - } - - self.local_user.manually_approves_followers = True - self.local_user.save(broadcast=False) - - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - views.inbox.activity_task(activity) - - # notification created - notification = models.Notification.objects.get() - self.assertEqual(notification.user, self.local_user) - self.assertEqual(notification.notification_type, "FOLLOW_REQUEST") - - # the request should exist - request = models.UserFollowRequest.objects.get() - self.assertEqual(request.user_subject, self.remote_user) - self.assertEqual(request.user_object, self.local_user) - - # the follow relationship should not exist - follow = models.UserFollows.objects.all() - self.assertEqual(list(follow), []) - - def test_handle_undo_follow_request(self): - """ the requester cancels a follow request """ - self.local_user.manually_approves_followers = True - self.local_user.save(broadcast=False) - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - request = models.UserFollowRequest.objects.create( - user_subject=self.remote_user, user_object=self.local_user - ) - self.assertTrue(self.local_user.follower_requests.exists()) - - activity = { - "type": "Undo", - "id": "bleh", - "to": ["https://www.w3.org/ns/activitystreams#Public"], - "cc": ["https://example.com/user/mouse/followers"], - "actor": self.remote_user.remote_id, - "@context": "https://www.w3.org/ns/activitystreams", - "object": { - "@context": "https://www.w3.org/ns/activitystreams", - "id": request.remote_id, - "type": "Follow", - "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse", - }, - } - - views.inbox.activity_task(activity) - - self.assertFalse(self.local_user.follower_requests.exists()) - - def test_handle_unfollow(self): - """ remove a relationship """ - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - rel = models.UserFollows.objects.create( - user_subject=self.remote_user, user_object=self.local_user - ) - activity = { - "type": "Undo", - "id": "bleh", - "to": ["https://www.w3.org/ns/activitystreams#Public"], - "cc": ["https://example.com/user/mouse/followers"], - "actor": self.remote_user.remote_id, - "@context": "https://www.w3.org/ns/activitystreams", - "object": { - "id": rel.remote_id, - "type": "Follow", - "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse", - }, - } - self.assertEqual(self.remote_user, self.local_user.followers.first()) - - views.inbox.activity_task(activity) - self.assertIsNone(self.local_user.followers.first()) - - def test_handle_follow_accept(self): - """ a remote user approved a follow request from local """ - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - rel = models.UserFollowRequest.objects.create( - user_subject=self.local_user, user_object=self.remote_user - ) - activity = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/users/rat/follows/123#accepts", - "type": "Accept", - "actor": "https://example.com/users/rat", - "object": { - "id": rel.remote_id, - "type": "Follow", - "actor": "https://example.com/user/mouse", - "object": "https://example.com/users/rat", - }, - } - - self.assertEqual(models.UserFollowRequest.objects.count(), 1) - - views.inbox.activity_task(activity) - - # request should be deleted - self.assertEqual(models.UserFollowRequest.objects.count(), 0) - - # relationship should be created - follows = self.remote_user.followers - self.assertEqual(follows.count(), 1) - self.assertEqual(follows.first(), self.local_user) - - def test_handle_follow_reject(self): - """ turn down a follow request """ - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - rel = models.UserFollowRequest.objects.create( - user_subject=self.local_user, user_object=self.remote_user - ) - activity = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/users/rat/follows/123#accepts", - "type": "Reject", - "actor": "https://example.com/users/rat", - "object": { - "id": rel.remote_id, - "type": "Follow", - "actor": "https://example.com/user/mouse", - "object": "https://example.com/users/rat", - }, - } - - self.assertEqual(models.UserFollowRequest.objects.count(), 1) - - views.inbox.activity_task(activity) - - # request should be deleted - self.assertFalse(models.UserFollowRequest.objects.exists()) - self.assertFalse(self.remote_user.followers.exists()) - - def test_handle_update_list(self): - """ a new list """ - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - book_list = models.List.objects.create( - name="hi", remote_id="https://example.com/list/22", user=self.local_user - ) - activity = { - "type": "Update", - "to": [], - "cc": [], - "actor": "hi", - "id": "sdkjf", - "object": { - "id": "https://example.com/list/22", - "type": "BookList", - "totalItems": 1, - "first": "https://example.com/list/22?page=1", - "last": "https://example.com/list/22?page=1", - "name": "Test List", - "owner": "https://example.com/user/mouse", - "to": ["https://www.w3.org/ns/activitystreams#Public"], - "cc": ["https://example.com/user/mouse/followers"], - "summary": "summary text", - "curation": "curated", - "@context": "https://www.w3.org/ns/activitystreams", - }, - } - views.inbox.activity_task(activity) - book_list.refresh_from_db() - self.assertEqual(book_list.name, "Test List") - self.assertEqual(book_list.curation, "curated") - self.assertEqual(book_list.description, "summary text") - self.assertEqual(book_list.remote_id, "https://example.com/list/22") - - def test_handle_delete_status(self): - """ remove a status """ - self.status.user = self.remote_user - self.status.save(broadcast=False) - - self.assertFalse(self.status.deleted) - activity = { - "type": "Delete", - "to": ["https://www.w3.org/ns/activitystreams#Public"], - "cc": ["https://example.com/user/mouse/followers"], - "id": "%s/activity" % self.status.remote_id, - "actor": self.remote_user.remote_id, - "object": {"id": self.status.remote_id, "type": "Tombstone"}, - } - with patch( - "bookwyrm.activitystreams.ActivityStream.remove_status" - ) as redis_mock: - views.inbox.activity_task(activity) - self.assertTrue(redis_mock.called) - # deletion doens't remove the status, it turns it into a tombstone - status = models.Status.objects.get() - self.assertTrue(status.deleted) - self.assertIsInstance(status.deleted_date, datetime) - - def test_handle_delete_status_notifications(self): - """ remove a status with related notifications """ - self.status.user = self.remote_user - self.status.save(broadcast=False) - models.Notification.objects.create( - related_status=self.status, - user=self.local_user, - notification_type="MENTION", - ) - # this one is innocent, don't delete it - notif = models.Notification.objects.create( - user=self.local_user, notification_type="MENTION" - ) - self.assertFalse(self.status.deleted) - self.assertEqual(models.Notification.objects.count(), 2) - activity = { - "type": "Delete", - "to": ["https://www.w3.org/ns/activitystreams#Public"], - "cc": ["https://example.com/user/mouse/followers"], - "id": "%s/activity" % self.status.remote_id, - "actor": self.remote_user.remote_id, - "object": {"id": self.status.remote_id, "type": "Tombstone"}, - } - with patch( - "bookwyrm.activitystreams.ActivityStream.remove_status" - ) as redis_mock: - views.inbox.activity_task(activity) - self.assertTrue(redis_mock.called) - # deletion doens't remove the status, it turns it into a tombstone - status = models.Status.objects.get() - self.assertTrue(status.deleted) - self.assertIsInstance(status.deleted_date, datetime) - - # notifications should be truly deleted - self.assertEqual(models.Notification.objects.count(), 1) - self.assertEqual(models.Notification.objects.get(), notif) - - def test_handle_favorite(self): - """ fav a status """ - activity = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/fav/1", - "actor": "https://example.com/users/rat", - "type": "Like", - "published": "Mon, 25 May 2020 19:31:20 GMT", - "object": self.status.remote_id, - } - - views.inbox.activity_task(activity) - - fav = models.Favorite.objects.get(remote_id="https://example.com/fav/1") - self.assertEqual(fav.status, self.status) - self.assertEqual(fav.remote_id, "https://example.com/fav/1") - self.assertEqual(fav.user, self.remote_user) - - def test_ignore_favorite(self): - """ don't try to save an unknown status """ - activity = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/fav/1", - "actor": "https://example.com/users/rat", - "type": "Like", - "published": "Mon, 25 May 2020 19:31:20 GMT", - "object": "https://unknown.status/not-found", - } - - views.inbox.activity_task(activity) - - self.assertFalse(models.Favorite.objects.exists()) - - def test_handle_unfavorite(self): - """ fav a status """ - activity = { - "id": "https://example.com/fav/1#undo", - "type": "Undo", - "to": ["https://www.w3.org/ns/activitystreams#Public"], - "cc": ["https://example.com/user/mouse/followers"], - "actor": self.remote_user.remote_id, - "object": { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/fav/1", - "actor": "https://example.com/users/rat", - "type": "Like", - "published": "Mon, 25 May 2020 19:31:20 GMT", - "object": self.status.remote_id, - }, - } - models.Favorite.objects.create( - status=self.status, - user=self.remote_user, - remote_id="https://example.com/fav/1", - ) - self.assertEqual(models.Favorite.objects.count(), 1) - - views.inbox.activity_task(activity) - self.assertEqual(models.Favorite.objects.count(), 0) - - @patch("bookwyrm.activitystreams.ActivityStream.add_status") - def test_handle_boost(self, _): - """ boost a status """ - self.assertEqual(models.Notification.objects.count(), 0) - activity = { - "type": "Announce", - "id": "%s/boost" % self.status.remote_id, - "actor": self.remote_user.remote_id, - "object": self.status.remote_id, - "to": ["https://www.w3.org/ns/activitystreams#public"], - "cc": ["https://example.com/user/mouse/followers"], - "published": "Mon, 25 May 2020 19:31:20 GMT", - } - with patch("bookwyrm.models.status.Status.ignore_activity") as discarder: - discarder.return_value = False - views.inbox.activity_task(activity) - boost = models.Boost.objects.get() - self.assertEqual(boost.boosted_status, self.status) - notification = models.Notification.objects.get() - self.assertEqual(notification.user, self.local_user) - self.assertEqual(notification.related_status, self.status) - - @responses.activate - @patch("bookwyrm.activitystreams.ActivityStream.add_status") - def test_handle_boost_remote_status(self, redis_mock): - """ boost a status """ - work = models.Work.objects.create(title="work title") - book = models.Edition.objects.create( - title="Test", - remote_id="https://bookwyrm.social/book/37292", - parent_work=work, - ) - self.assertEqual(models.Notification.objects.count(), 0) - activity = { - "type": "Announce", - "id": "%s/boost" % self.status.remote_id, - "actor": self.remote_user.remote_id, - "object": "https://remote.com/status/1", - "to": ["https://www.w3.org/ns/activitystreams#public"], - "cc": ["https://example.com/user/mouse/followers"], - "published": "Mon, 25 May 2020 19:31:20 GMT", - } - responses.add( - responses.GET, - "https://remote.com/status/1", - json={ - "id": "https://remote.com/status/1", - "type": "Comment", - "published": "2021-04-05T18:04:59.735190+00:00", - "attributedTo": self.remote_user.remote_id, - "content": "

    a comment

    ", - "to": ["https://www.w3.org/ns/activitystreams#Public"], - "cc": ["https://b875df3d118b.ngrok.io/user/mouse/followers"], - "inReplyTo": "", - "inReplyToBook": book.remote_id, - "summary": "", - "tag": [], - "sensitive": False, - "@context": "https://www.w3.org/ns/activitystreams", - }, - ) - - with patch("bookwyrm.models.status.Status.ignore_activity") as discarder: - discarder.return_value = False - views.inbox.activity_task(activity) - self.assertTrue(redis_mock.called) - - boost = models.Boost.objects.get() - self.assertEqual(boost.boosted_status.remote_id, "https://remote.com/status/1") - self.assertEqual(boost.boosted_status.comment.status_type, "Comment") - self.assertEqual(boost.boosted_status.comment.book, book) - - @responses.activate - def test_handle_discarded_boost(self): - """ test a boost of a mastodon status that will be discarded """ - status = models.Status( - content="hi", - user=self.remote_user, - ) - with patch("bookwyrm.activitystreams.ActivityStream.add_status"): - status.save(broadcast=False) - activity = { - "type": "Announce", - "id": "http://www.faraway.com/boost/12", - "actor": self.remote_user.remote_id, - "object": status.remote_id, - } - responses.add( - responses.GET, status.remote_id, json=status.to_activity(), status=200 - ) - views.inbox.activity_task(activity) - self.assertEqual(models.Boost.objects.count(), 0) - - def test_handle_unboost(self): - """ undo a boost """ - with patch("bookwyrm.activitystreams.ActivityStream.add_status"): - boost = models.Boost.objects.create( - boosted_status=self.status, user=self.remote_user - ) - activity = { - "type": "Undo", - "actor": "hi", - "id": "bleh", - "to": ["https://www.w3.org/ns/activitystreams#public"], - "cc": ["https://example.com/user/mouse/followers"], - "object": { - "type": "Announce", - "id": boost.remote_id, - "actor": self.remote_user.remote_id, - "object": self.status.remote_id, - "to": ["https://www.w3.org/ns/activitystreams#public"], - "cc": ["https://example.com/user/mouse/followers"], - "published": "Mon, 25 May 2020 19:31:20 GMT", - }, - } - with patch( - "bookwyrm.activitystreams.ActivityStream.remove_status" - ) as redis_mock: - views.inbox.activity_task(activity) - self.assertTrue(redis_mock.called) - self.assertFalse(models.Boost.objects.exists()) - - def test_handle_unboost_unknown_boost(self): - """ undo a boost """ - activity = { - "type": "Undo", - "actor": "hi", - "id": "bleh", - "to": ["https://www.w3.org/ns/activitystreams#public"], - "cc": ["https://example.com/user/mouse/followers"], - "object": { - "type": "Announce", - "id": "http://fake.com/unknown/boost", - "actor": self.remote_user.remote_id, - "object": self.status.remote_id, - }, - } - views.inbox.activity_task(activity) - - def test_handle_add_book_to_shelf(self): - """ shelving a book """ - work = models.Work.objects.create(title="work title") - book = models.Edition.objects.create( - title="Test", - remote_id="https://bookwyrm.social/book/37292", - parent_work=work, - ) - shelf = models.Shelf.objects.create(user=self.remote_user, name="Test Shelf") - shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read" - shelf.save() - - activity = { - "id": "https://bookwyrm.social/shelfbook/6189#add", - "type": "Add", - "actor": "https://example.com/users/rat", - "object": { - "type": "Edition", - "title": "Test Title", - "work": work.remote_id, - "id": "https://bookwyrm.social/book/37292", - }, - "target": "https://bookwyrm.social/user/mouse/shelf/to-read", - "@context": "https://www.w3.org/ns/activitystreams", - } - views.inbox.activity_task(activity) - self.assertEqual(shelf.books.first(), book) - - def test_handle_unshelve_book(self): - """ remove a book from a shelf """ - work = models.Work.objects.create(title="work title") - book = models.Edition.objects.create( - title="Test", - remote_id="https://bookwyrm.social/book/37292", - parent_work=work, - ) - shelf = models.Shelf.objects.create(user=self.remote_user, name="Test Shelf") - shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read" - shelf.save() - - shelfbook = models.ShelfBook.objects.create( - user=self.remote_user, shelf=shelf, book=book - ) - - self.assertEqual(shelf.books.first(), book) - self.assertEqual(shelf.books.count(), 1) - - activity = { - "id": shelfbook.remote_id, - "type": "Remove", - "actor": "https://example.com/users/rat", - "object": { - "type": "Edition", - "title": "Test Title", - "work": work.remote_id, - "id": "https://bookwyrm.social/book/37292", - }, - "target": "https://bookwyrm.social/user/mouse/shelf/to-read", - "@context": "https://www.w3.org/ns/activitystreams", - } - views.inbox.activity_task(activity) - self.assertFalse(shelf.books.exists()) - - @responses.activate - def test_handle_add_book_to_list(self): - """ listing a book """ - work = models.Work.objects.create(title="work title") - book = models.Edition.objects.create( - title="Test", - remote_id="https://bookwyrm.social/book/37292", - parent_work=work, - ) - - responses.add( - responses.GET, - "https://bookwyrm.social/user/mouse/list/to-read", - json={ - "id": "https://example.com/list/22", - "type": "BookList", - "totalItems": 1, - "first": "https://example.com/list/22?page=1", - "last": "https://example.com/list/22?page=1", - "name": "Test List", - "owner": "https://example.com/user/mouse", - "to": ["https://www.w3.org/ns/activitystreams#Public"], - "cc": ["https://example.com/user/mouse/followers"], - "summary": "summary text", - "curation": "curated", - "@context": "https://www.w3.org/ns/activitystreams", - }, - ) - - activity = { - "id": "https://bookwyrm.social/listbook/6189#add", - "type": "Add", - "actor": "https://example.com/users/rat", - "object": { - "type": "Edition", - "title": "Test Title", - "work": work.remote_id, - "id": "https://bookwyrm.social/book/37292", - }, - "target": "https://bookwyrm.social/user/mouse/list/to-read", - "@context": "https://www.w3.org/ns/activitystreams", - } - views.inbox.activity_task(activity) - - booklist = models.List.objects.get() - self.assertEqual(booklist.name, "Test List") - self.assertEqual(booklist.books.first(), book) - - @responses.activate - def test_handle_tag_book(self): - """ listing a book """ - work = models.Work.objects.create(title="work title") - book = models.Edition.objects.create( - title="Test", - remote_id="https://bookwyrm.social/book/37292", - parent_work=work, - ) - - responses.add( - responses.GET, - "https://www.example.com/tag/cool-tag", - json={ - "id": "https://1b1a78582461.ngrok.io/tag/tag", - "type": "OrderedCollection", - "totalItems": 0, - "first": "https://1b1a78582461.ngrok.io/tag/tag?page=1", - "last": "https://1b1a78582461.ngrok.io/tag/tag?page=1", - "name": "cool tag", - "@context": "https://www.w3.org/ns/activitystreams", - }, - ) - - activity = { - "id": "https://bookwyrm.social/listbook/6189#add", - "type": "Add", - "actor": "https://example.com/users/rat", - "object": { - "type": "Edition", - "title": "Test Title", - "work": work.remote_id, - "id": "https://bookwyrm.social/book/37292", - }, - "target": "https://www.example.com/tag/cool-tag", - "@context": "https://www.w3.org/ns/activitystreams", - } - views.inbox.activity_task(activity) - - tag = models.Tag.objects.get() - self.assertFalse(models.List.objects.exists()) - self.assertEqual(tag.name, "cool tag") - self.assertEqual(tag.books.first(), book) - - def test_handle_update_user(self): - """ update an existing user """ - # we only do this with remote users - self.local_user.local = False - self.local_user.save() - - datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") - userdata = json.loads(datafile.read_bytes()) - del userdata["icon"] - self.assertIsNone(self.local_user.name) - views.inbox.activity_task( - { - "type": "Update", - "to": [], - "cc": [], - "actor": "hi", - "id": "sdkjf", - "object": userdata, - } - ) - user = models.User.objects.get(id=self.local_user.id) - self.assertEqual(user.name, "MOUSE?? MOUSE!!") - self.assertEqual(user.username, "mouse@example.com") - self.assertEqual(user.localname, "mouse") - self.assertTrue(user.discoverable) - - def test_handle_update_edition(self): - """ update an existing edition """ - datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_edition.json") - bookdata = json.loads(datafile.read_bytes()) - - models.Work.objects.create( - title="Test Work", remote_id="https://bookwyrm.social/book/5988" - ) - book = models.Edition.objects.create( - title="Test Book", remote_id="https://bookwyrm.social/book/5989" - ) - - del bookdata["authors"] - self.assertEqual(book.title, "Test Book") - - with patch("bookwyrm.activitypub.base_activity.set_related_field.delay"): - views.inbox.activity_task( - { - "type": "Update", - "to": [], - "cc": [], - "actor": "hi", - "id": "sdkjf", - "object": bookdata, - } - ) - book = models.Edition.objects.get(id=book.id) - self.assertEqual(book.title, "Piranesi") - - def test_handle_update_work(self): - """ update an existing edition """ - datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_work.json") - bookdata = json.loads(datafile.read_bytes()) - - book = models.Work.objects.create( - title="Test Book", remote_id="https://bookwyrm.social/book/5988" - ) - - del bookdata["authors"] - self.assertEqual(book.title, "Test Book") - with patch("bookwyrm.activitypub.base_activity.set_related_field.delay"): - views.inbox.activity_task( - { - "type": "Update", - "to": [], - "cc": [], - "actor": "hi", - "id": "sdkjf", - "object": bookdata, - } - ) - book = models.Work.objects.get(id=book.id) - self.assertEqual(book.title, "Piranesi") - - def test_handle_blocks(self): - """ create a "block" database entry from an activity """ - self.local_user.followers.add(self.remote_user) - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - models.UserFollowRequest.objects.create( - user_subject=self.local_user, user_object=self.remote_user - ) - self.assertTrue(models.UserFollows.objects.exists()) - self.assertTrue(models.UserFollowRequest.objects.exists()) - - activity = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/9e1f41ac-9ddd-4159", - "type": "Block", - "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse", - } - - with patch( - "bookwyrm.activitystreams.ActivityStream.remove_user_statuses" - ) as redis_mock: - views.inbox.activity_task(activity) - self.assertTrue(redis_mock.called) - views.inbox.activity_task(activity) - block = models.UserBlocks.objects.get() - self.assertEqual(block.user_subject, self.remote_user) - self.assertEqual(block.user_object, self.local_user) - self.assertEqual(block.remote_id, "https://example.com/9e1f41ac-9ddd-4159") - - self.assertFalse(models.UserFollows.objects.exists()) - self.assertFalse(models.UserFollowRequest.objects.exists()) - - def test_handle_unblock(self): - """ unblock a user """ - self.remote_user.blocks.add(self.local_user) - - block = models.UserBlocks.objects.get() - block.remote_id = "https://example.com/9e1f41ac-9ddd-4159" - block.save() - - self.assertEqual(block.user_subject, self.remote_user) - self.assertEqual(block.user_object, self.local_user) - activity = { - "type": "Undo", - "actor": "hi", - "id": "bleh", - "to": ["https://www.w3.org/ns/activitystreams#public"], - "cc": ["https://example.com/user/mouse/followers"], - "object": { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/9e1f41ac-9ddd-4159", - "type": "Block", - "actor": "https://example.com/users/rat", - "object": "https://example.com/user/mouse", - }, - } - with patch( - "bookwyrm.activitystreams.ActivityStream.add_user_statuses" - ) as redis_mock: - views.inbox.activity_task(activity) - self.assertTrue(redis_mock.called) - self.assertFalse(models.UserBlocks.objects.exists()) diff --git a/bookwyrm/tests/views/test_interaction.py b/bookwyrm/tests/views/test_interaction.py index 297eeb73..876d6053 100644 --- a/bookwyrm/tests/views/test_interaction.py +++ b/bookwyrm/tests/views/test_interaction.py @@ -1,4 +1,5 @@ """ test for app action functionality """ +import json from unittest.mock import patch from django.test import TestCase from django.test.client import RequestFactory @@ -8,10 +9,10 @@ from bookwyrm import models, views @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class InteractionViews(TestCase): - """ viewing and creating statuses """ + """viewing and creating statuses""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -39,8 +40,8 @@ class InteractionViews(TestCase): parent_work=work, ) - def test_handle_favorite(self, _): - """ create and broadcast faving a status """ + def test_favorite(self, _): + """create and broadcast faving a status""" view = views.Favorite.as_view() request = self.factory.post("") request.user = self.remote_user @@ -57,8 +58,8 @@ class InteractionViews(TestCase): self.assertEqual(notification.user, self.local_user) self.assertEqual(notification.related_user, self.remote_user) - def test_handle_unfavorite(self, _): - """ unfav a status """ + def test_unfavorite(self, _): + """unfav a status""" view = views.Unfavorite.as_view() request = self.factory.post("") request.user = self.remote_user @@ -74,8 +75,8 @@ class InteractionViews(TestCase): self.assertEqual(models.Favorite.objects.count(), 0) self.assertEqual(models.Notification.objects.count(), 0) - def test_handle_boost(self, _): - """ boost a status """ + def test_boost(self, _): + """boost a status""" view = views.Boost.as_view() request = self.factory.post("") request.user = self.remote_user @@ -85,6 +86,7 @@ class InteractionViews(TestCase): view(request, status.id) boost = models.Boost.objects.get() + self.assertEqual(boost.boosted_status, status) self.assertEqual(boost.user, self.remote_user) self.assertEqual(boost.privacy, "public") @@ -95,15 +97,22 @@ class InteractionViews(TestCase): self.assertEqual(notification.related_user, self.remote_user) self.assertEqual(notification.related_status, status) - def test_handle_self_boost(self, _): - """ boost your own status """ + def test_self_boost(self, _): + """boost your own status""" view = views.Boost.as_view() request = self.factory.post("") request.user = self.local_user with patch("bookwyrm.activitystreams.ActivityStream.add_status"): status = models.Status.objects.create(user=self.local_user, content="hi") - view(request, status.id) + with patch( + "bookwyrm.models.activitypub_mixin.broadcast_task.delay" + ) as broadcast_mock: + view(request, status.id) + + self.assertEqual(broadcast_mock.call_count, 1) + activity = json.loads(broadcast_mock.call_args[0][1]) + self.assertEqual(activity["type"], "Announce") boost = models.Boost.objects.get() self.assertEqual(boost.boosted_status, status) @@ -112,8 +121,8 @@ class InteractionViews(TestCase): self.assertFalse(models.Notification.objects.exists()) - def test_handle_boost_unlisted(self, _): - """ boost a status """ + def test_boost_unlisted(self, _): + """boost a status""" view = views.Boost.as_view() request = self.factory.post("") request.user = self.local_user @@ -127,8 +136,8 @@ class InteractionViews(TestCase): boost = models.Boost.objects.get() self.assertEqual(boost.privacy, "unlisted") - def test_handle_boost_private(self, _): - """ boost a status """ + def test_boost_private(self, _): + """boost a status""" view = views.Boost.as_view() request = self.factory.post("") request.user = self.local_user @@ -140,8 +149,8 @@ class InteractionViews(TestCase): view(request, status.id) self.assertFalse(models.Boost.objects.exists()) - def test_handle_boost_twice(self, _): - """ boost a status """ + def test_boost_twice(self, _): + """boost a status""" view = views.Boost.as_view() request = self.factory.post("") request.user = self.local_user @@ -152,8 +161,8 @@ class InteractionViews(TestCase): view(request, status.id) self.assertEqual(models.Boost.objects.count(), 1) - def test_handle_unboost(self, _): - """ undo a boost """ + def test_unboost(self, _): + """undo a boost""" view = views.Unboost.as_view() request = self.factory.post("") request.user = self.remote_user @@ -164,7 +173,7 @@ class InteractionViews(TestCase): self.assertEqual(models.Boost.objects.count(), 1) self.assertEqual(models.Notification.objects.count(), 1) with patch( - "bookwyrm.activitystreams.ActivityStream.remove_status" + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" ) as redis_mock: view(request, status.id) self.assertTrue(redis_mock.called) diff --git a/bookwyrm/tests/views/test_invite.py b/bookwyrm/tests/views/test_invite.py index 7bfc8fe5..7b5071b3 100644 --- a/bookwyrm/tests/views/test_invite.py +++ b/bookwyrm/tests/views/test_invite.py @@ -11,10 +11,10 @@ from bookwyrm import views class InviteViews(TestCase): - """ every response to a get request, html or json """ + """every response to a get request, html or json""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -26,7 +26,7 @@ class InviteViews(TestCase): models.SiteSettings.objects.create() def test_invite_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Invite.as_view() models.SiteInvite.objects.create(code="hi", user=self.local_user) request = self.factory.get("") @@ -41,7 +41,7 @@ class InviteViews(TestCase): self.assertEqual(result.status_code, 200) def test_manage_invites(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.ManageInvites.as_view() request = self.factory.get("") request.user = self.local_user @@ -52,7 +52,7 @@ class InviteViews(TestCase): self.assertEqual(result.status_code, 200) def test_invite_request(self): - """ request to join a server """ + """request to join a server""" form = forms.InviteRequestForm() form.data["email"] = "new@user.email" @@ -66,7 +66,7 @@ class InviteViews(TestCase): self.assertEqual(req.email, "new@user.email") def test_invite_request_email_taken(self): - """ request to join a server with an existing user email """ + """request to join a server with an existing user email""" form = forms.InviteRequestForm() form.data["email"] = "mouse@mouse.mouse" @@ -80,7 +80,7 @@ class InviteViews(TestCase): self.assertFalse(models.InviteRequest.objects.exists()) def test_manage_invite_requests(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.ManageInviteRequests.as_view() request = self.factory.get("") request.user = self.local_user @@ -98,7 +98,7 @@ class InviteViews(TestCase): self.assertEqual(result.status_code, 200) def test_manage_invite_requests_send(self): - """ send an invite """ + """send an invite""" req = models.InviteRequest.objects.create(email="fish@example.com") view = views.ManageInviteRequests.as_view() @@ -113,7 +113,7 @@ class InviteViews(TestCase): self.assertIsNotNone(req.invite) def test_ignore_invite_request(self): - """ don't invite that jerk """ + """don't invite that jerk""" req = models.InviteRequest.objects.create(email="fish@example.com") view = views.ignore_invite_request diff --git a/bookwyrm/tests/views/test_isbn.py b/bookwyrm/tests/views/test_isbn.py index 7f03a610..2aedd3ce 100644 --- a/bookwyrm/tests/views/test_isbn.py +++ b/bookwyrm/tests/views/test_isbn.py @@ -11,10 +11,10 @@ from bookwyrm.settings import DOMAIN class IsbnViews(TestCase): - """ tag views""" + """tag views""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -37,7 +37,7 @@ class IsbnViews(TestCase): models.SiteSettings.objects.create() def test_isbn_json_response(self): - """ searches local data only and returns book data in json format """ + """searches local data only and returns book data in json format""" view = views.Isbn.as_view() request = self.factory.get("") with patch("bookwyrm.views.isbn.is_api_request") as is_api: diff --git a/bookwyrm/tests/views/test_landing.py b/bookwyrm/tests/views/test_landing.py index 2513fecc..864e48f7 100644 --- a/bookwyrm/tests/views/test_landing.py +++ b/bookwyrm/tests/views/test_landing.py @@ -10,10 +10,10 @@ from bookwyrm import views class LandingViews(TestCase): - """ pages you land on without really trying """ + """pages you land on without really trying""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -27,7 +27,7 @@ class LandingViews(TestCase): models.SiteSettings.objects.create() def test_home_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Home.as_view() request = self.factory.get("") request.user = self.local_user @@ -43,7 +43,7 @@ class LandingViews(TestCase): result.render() def test_about_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.About.as_view() request = self.factory.get("") request.user = self.local_user @@ -53,7 +53,7 @@ class LandingViews(TestCase): self.assertEqual(result.status_code, 200) def test_discover(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Discover.as_view() request = self.factory.get("") result = view(request) diff --git a/bookwyrm/tests/views/test_list.py b/bookwyrm/tests/views/test_list.py index c2c75453..3de35b1e 100644 --- a/bookwyrm/tests/views/test_list.py +++ b/bookwyrm/tests/views/test_list.py @@ -1,4 +1,5 @@ """ test for app action functionality """ +import json from unittest.mock import patch from django.contrib.auth.models import AnonymousUser @@ -11,10 +12,10 @@ from bookwyrm.activitypub import ActivitypubResponse # pylint: disable=unused-argument class ListViews(TestCase): - """ tag views""" + """tag views""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -38,6 +39,25 @@ class ListViews(TestCase): remote_id="https://example.com/book/1", parent_work=work, ) + work_two = models.Work.objects.create(title="Labori") + self.book_two = models.Edition.objects.create( + title="Example Edition 2", + remote_id="https://example.com/book/2", + parent_work=work_two, + ) + work_three = models.Work.objects.create(title="Trabajar") + self.book_three = models.Edition.objects.create( + title="Example Edition 3", + remote_id="https://example.com/book/3", + parent_work=work_three, + ) + work_four = models.Work.objects.create(title="Travailler") + self.book_four = models.Edition.objects.create( + title="Example Edition 4", + remote_id="https://example.com/book/4", + parent_work=work_four, + ) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): self.list = models.List.objects.create( name="Test List", user=self.local_user @@ -47,7 +67,7 @@ class ListViews(TestCase): models.SiteSettings.objects.create() def test_lists_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Lists.as_view() with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.List.objects.create(name="Public list", user=self.local_user) @@ -70,17 +90,7 @@ class ListViews(TestCase): self.assertEqual(result.status_code, 200) def test_lists_create(self): - """ create list view """ - real_broadcast = models.List.broadcast - - def mock_broadcast(_, activity, user, **kwargs): - """ ok """ - self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity["type"], "Create") - self.assertEqual(activity["actor"], self.local_user.remote_id) - - models.List.broadcast = mock_broadcast - + """create list view""" view = views.Lists.as_view() request = self.factory.post( "", @@ -93,16 +103,22 @@ class ListViews(TestCase): }, ) request.user = self.local_user - result = view(request) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + result = view(request) + + self.assertEqual(mock.call_count, 1) + activity = json.loads(mock.call_args[0][1]) + self.assertEqual(activity["type"], "Create") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(result.status_code, 302) new_list = models.List.objects.filter(name="A list").get() self.assertEqual(new_list.description, "wow") self.assertEqual(new_list.privacy, "unlisted") self.assertEqual(new_list.curation, "open") - models.List.broadcast = real_broadcast def test_list_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.List.as_view() request = self.factory.get("") request.user = self.local_user @@ -137,18 +153,7 @@ class ListViews(TestCase): self.assertEqual(result.status_code, 200) def test_list_edit(self): - """ edit a list """ - real_broadcast = models.List.broadcast - - def mock_broadcast(_, activity, user, **kwargs): - """ ok """ - self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity["type"], "Update") - self.assertEqual(activity["actor"], self.local_user.remote_id) - self.assertEqual(activity["object"]["id"], self.list.remote_id) - - models.List.broadcast = mock_broadcast - + """edit a list""" view = views.List.as_view() request = self.factory.post( "", @@ -162,7 +167,15 @@ class ListViews(TestCase): ) request.user = self.local_user - result = view(request, self.list.id) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + result = view(request, self.list.id) + + self.assertEqual(mock.call_count, 1) + activity = json.loads(mock.call_args[0][1]) + self.assertEqual(activity["type"], "Update") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["object"]["id"], self.list.remote_id) + self.assertEqual(result.status_code, 302) self.list.refresh_from_db() @@ -170,10 +183,9 @@ class ListViews(TestCase): self.assertEqual(self.list.description, "wow") self.assertEqual(self.list.privacy, "direct") self.assertEqual(self.list.curation, "curated") - models.List.broadcast = real_broadcast def test_curate_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Curate.as_view() with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.List.objects.create(name="Public list", user=self.local_user) @@ -193,18 +205,7 @@ class ListViews(TestCase): self.assertEqual(result.status_code, 302) def test_curate_approve(self): - """ approve a pending item """ - real_broadcast = models.List.broadcast - - def mock_broadcast(_, activity, user, **kwargs): - """ ok """ - self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity["type"], "Add") - self.assertEqual(activity["actor"], self.local_user.remote_id) - self.assertEqual(activity["target"], self.list.remote_id) - - models.ListItem.broadcast = mock_broadcast - + """approve a pending item""" view = views.Curate.as_view() with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): pending = models.ListItem.objects.create( @@ -212,6 +213,7 @@ class ListViews(TestCase): user=self.local_user, book=self.book, approved=False, + order=1, ) request = self.factory.post( @@ -223,15 +225,22 @@ class ListViews(TestCase): ) request.user = self.local_user - view(request, self.list.id) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + view(request, self.list.id) + + self.assertEqual(mock.call_count, 2) + activity = json.loads(mock.call_args[0][1]) + self.assertEqual(activity["type"], "Add") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["target"], self.list.remote_id) + pending.refresh_from_db() self.assertEqual(self.list.books.count(), 1) self.assertEqual(self.list.listitem_set.first(), pending) self.assertTrue(pending.approved) - models.ListItem.broadcast = real_broadcast def test_curate_reject(self): - """ approve a pending item """ + """approve a pending item""" view = views.Curate.as_view() with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): pending = models.ListItem.objects.create( @@ -239,6 +248,7 @@ class ListViews(TestCase): user=self.local_user, book=self.book, approved=False, + order=1, ) request = self.factory.post( @@ -250,23 +260,13 @@ class ListViews(TestCase): ) request.user = self.local_user - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - view(request, self.list.id) + view(request, self.list.id) + self.assertFalse(self.list.books.exists()) self.assertFalse(models.ListItem.objects.exists()) def test_add_book(self): - """ put a book on a list """ - real_broadcast = models.List.broadcast - - def mock_broadcast(_, activity, user): - """ ok """ - self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity["type"], "Add") - self.assertEqual(activity["actor"], self.local_user.remote_id) - self.assertEqual(activity["target"], self.list.remote_id) - - models.ListItem.broadcast = mock_broadcast + """put a book on a list""" request = self.factory.post( "", { @@ -276,25 +276,276 @@ class ListViews(TestCase): ) request.user = self.local_user - views.list.add_book(request) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + views.list.add_book(request) + self.assertEqual(mock.call_count, 1) + activity = json.loads(mock.call_args[0][1]) + self.assertEqual(activity["type"], "Add") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["target"], self.list.remote_id) + item = self.list.listitem_set.get() self.assertEqual(item.book, self.book) self.assertEqual(item.user, self.local_user) self.assertTrue(item.approved) - models.ListItem.broadcast = real_broadcast + + def test_add_two_books(self): + """ + Putting two books on the list. The first should have an order value of + 1 and the second should have an order value of 2. + """ + request_one = self.factory.post( + "", + { + "book": self.book.id, + "list": self.list.id, + }, + ) + request_one.user = self.local_user + + request_two = self.factory.post( + "", + { + "book": self.book_two.id, + "list": self.list.id, + }, + ) + request_two.user = self.local_user + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.list.add_book(request_one) + views.list.add_book(request_two) + + items = self.list.listitem_set.order_by("order").all() + self.assertEqual(items[0].book, self.book) + self.assertEqual(items[1].book, self.book_two) + self.assertEqual(items[0].order, 1) + self.assertEqual(items[1].order, 2) + + def test_add_three_books_and_remove_second(self): + """ + Put three books on a list and then remove the one in the middle. The + ordering of the list should adjust to not have a gap. + """ + request_one = self.factory.post( + "", + { + "book": self.book.id, + "list": self.list.id, + }, + ) + request_one.user = self.local_user + + request_two = self.factory.post( + "", + { + "book": self.book_two.id, + "list": self.list.id, + }, + ) + request_two.user = self.local_user + + request_three = self.factory.post( + "", + { + "book": self.book_three.id, + "list": self.list.id, + }, + ) + request_three.user = self.local_user + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.list.add_book(request_one) + views.list.add_book(request_two) + views.list.add_book(request_three) + + items = self.list.listitem_set.order_by("order").all() + self.assertEqual(items[0].book, self.book) + self.assertEqual(items[1].book, self.book_two) + self.assertEqual(items[2].book, self.book_three) + self.assertEqual(items[0].order, 1) + self.assertEqual(items[1].order, 2) + self.assertEqual(items[2].order, 3) + + remove_request = self.factory.post("", {"item": items[1].id}) + remove_request.user = self.local_user + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.list.remove_book(remove_request, self.list.id) + items = self.list.listitem_set.order_by("order").all() + self.assertEqual(items[0].book, self.book) + self.assertEqual(items[1].book, self.book_three) + self.assertEqual(items[0].order, 1) + self.assertEqual(items[1].order, 2) + + def test_adding_book_with_a_pending_book(self): + """ + When a list contains any pending books, the pending books should have + be at the end of the list by order. If a book is added while a book is + pending, its order should precede the pending books. + """ + request = self.factory.post( + "", + { + "book": self.book_three.id, + "list": self.list.id, + }, + ) + request.user = self.local_user + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + models.ListItem.objects.create( + book_list=self.list, + user=self.local_user, + book=self.book, + approved=True, + order=1, + ) + models.ListItem.objects.create( + book_list=self.list, + user=self.rat, + book=self.book_two, + approved=False, + order=2, + ) + views.list.add_book(request) + + items = self.list.listitem_set.order_by("order").all() + self.assertEqual(items[0].book, self.book) + self.assertEqual(items[0].order, 1) + self.assertTrue(items[0].approved) + + self.assertEqual(items[1].book, self.book_three) + self.assertEqual(items[1].order, 2) + self.assertTrue(items[1].approved) + + self.assertEqual(items[2].book, self.book_two) + self.assertEqual(items[2].order, 3) + self.assertFalse(items[2].approved) + + def test_approving_one_pending_book_from_multiple(self): + """ + When a list contains any pending books, the pending books should have + be at the end of the list by order. If a pending book is approved, then + its order should be at the end of the approved books and before the + remaining pending books. + """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + models.ListItem.objects.create( + book_list=self.list, + user=self.local_user, + book=self.book, + approved=True, + order=1, + ) + models.ListItem.objects.create( + book_list=self.list, + user=self.local_user, + book=self.book_two, + approved=True, + order=2, + ) + models.ListItem.objects.create( + book_list=self.list, + user=self.rat, + book=self.book_three, + approved=False, + order=3, + ) + to_be_approved = models.ListItem.objects.create( + book_list=self.list, + user=self.rat, + book=self.book_four, + approved=False, + order=4, + ) + + view = views.Curate.as_view() + request = self.factory.post( + "", + { + "item": to_be_approved.id, + "approved": "true", + }, + ) + request.user = self.local_user + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + view(request, self.list.id) + + items = self.list.listitem_set.order_by("order").all() + self.assertEqual(items[0].book, self.book) + self.assertEqual(items[0].order, 1) + self.assertTrue(items[0].approved) + + self.assertEqual(items[1].book, self.book_two) + self.assertEqual(items[1].order, 2) + self.assertTrue(items[1].approved) + + self.assertEqual(items[2].book, self.book_four) + self.assertEqual(items[2].order, 3) + self.assertTrue(items[2].approved) + + self.assertEqual(items[3].book, self.book_three) + self.assertEqual(items[3].order, 4) + self.assertFalse(items[3].approved) + + def test_add_three_books_and_move_last_to_first(self): + """ + Put three books on the list and move the last book to the first + position. + """ + request_one = self.factory.post( + "", + { + "book": self.book.id, + "list": self.list.id, + }, + ) + request_one.user = self.local_user + + request_two = self.factory.post( + "", + { + "book": self.book_two.id, + "list": self.list.id, + }, + ) + request_two.user = self.local_user + + request_three = self.factory.post( + "", + { + "book": self.book_three.id, + "list": self.list.id, + }, + ) + request_three.user = self.local_user + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.list.add_book(request_one) + views.list.add_book(request_two) + views.list.add_book(request_three) + + items = self.list.listitem_set.order_by("order").all() + self.assertEqual(items[0].book, self.book) + self.assertEqual(items[1].book, self.book_two) + self.assertEqual(items[2].book, self.book_three) + self.assertEqual(items[0].order, 1) + self.assertEqual(items[1].order, 2) + self.assertEqual(items[2].order, 3) + + set_position_request = self.factory.post("", {"position": 1}) + set_position_request.user = self.local_user + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.list.set_book_position(set_position_request, items[2].id) + items = self.list.listitem_set.order_by("order").all() + self.assertEqual(items[0].book, self.book_three) + self.assertEqual(items[1].book, self.book) + self.assertEqual(items[2].book, self.book_two) + self.assertEqual(items[0].order, 1) + self.assertEqual(items[1].order, 2) + self.assertEqual(items[2].order, 3) def test_add_book_outsider(self): - """ put a book on a list """ - real_broadcast = models.List.broadcast - - def mock_broadcast(_, activity, user): - """ ok """ - self.assertEqual(user.remote_id, self.rat.remote_id) - self.assertEqual(activity["type"], "Add") - self.assertEqual(activity["actor"], self.rat.remote_id) - self.assertEqual(activity["target"], self.list.remote_id) - - models.ListItem.broadcast = mock_broadcast + """put a book on a list""" self.list.curation = "open" self.list.save(broadcast=False) request = self.factory.post( @@ -306,26 +557,21 @@ class ListViews(TestCase): ) request.user = self.rat - views.list.add_book(request) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + views.list.add_book(request) + self.assertEqual(mock.call_count, 1) + activity = json.loads(mock.call_args[0][1]) + self.assertEqual(activity["type"], "Add") + self.assertEqual(activity["actor"], self.rat.remote_id) + self.assertEqual(activity["target"], self.list.remote_id) + item = self.list.listitem_set.get() self.assertEqual(item.book, self.book) self.assertEqual(item.user, self.rat) self.assertTrue(item.approved) - models.ListItem.broadcast = real_broadcast def test_add_book_pending(self): - """ put a book on a list awaiting approval """ - real_broadcast = models.List.broadcast - - def mock_broadcast(_, activity, user): - """ ok """ - self.assertEqual(user.remote_id, self.rat.remote_id) - self.assertEqual(activity["type"], "Add") - self.assertEqual(activity["actor"], self.rat.remote_id) - self.assertEqual(activity["target"], self.list.remote_id) - self.assertEqual(activity["object"]["id"], self.book.remote_id) - - models.ListItem.broadcast = mock_broadcast + """put a book on a list awaiting approval""" self.list.curation = "curated" self.list.save(broadcast=False) request = self.factory.post( @@ -337,26 +583,25 @@ class ListViews(TestCase): ) request.user = self.rat - views.list.add_book(request) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + views.list.add_book(request) + + self.assertEqual(mock.call_count, 1) + activity = json.loads(mock.call_args[0][1]) + + self.assertEqual(activity["type"], "Add") + self.assertEqual(activity["actor"], self.rat.remote_id) + self.assertEqual(activity["target"], self.list.remote_id) + item = self.list.listitem_set.get() + self.assertEqual(activity["object"]["id"], item.remote_id) + self.assertEqual(item.book, self.book) self.assertEqual(item.user, self.rat) self.assertFalse(item.approved) - models.ListItem.broadcast = real_broadcast def test_add_book_self_curated(self): - """ put a book on a list automatically approved """ - real_broadcast = models.ListItem.broadcast - - def mock_broadcast(_, activity, user): - """ ok """ - self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity["type"], "Add") - self.assertEqual(activity["actor"], self.local_user.remote_id) - self.assertEqual(activity["target"], self.list.remote_id) - - models.ListItem.broadcast = mock_broadcast - + """put a book on a list automatically approved""" self.list.curation = "curated" self.list.save(broadcast=False) request = self.factory.post( @@ -368,33 +613,31 @@ class ListViews(TestCase): ) request.user = self.local_user - views.list.add_book(request) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + views.list.add_book(request) + self.assertEqual(mock.call_count, 1) + activity = json.loads(mock.call_args[0][1]) + self.assertEqual(activity["type"], "Add") + self.assertEqual(activity["actor"], self.local_user.remote_id) + self.assertEqual(activity["target"], self.list.remote_id) + item = self.list.listitem_set.get() self.assertEqual(item.book, self.book) self.assertEqual(item.user, self.local_user) self.assertTrue(item.approved) - models.ListItem.broadcast = real_broadcast def test_remove_book(self): - """ take an item off a list """ - real_broadcast = models.ListItem.broadcast + """take an item off a list""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): item = models.ListItem.objects.create( book_list=self.list, user=self.local_user, book=self.book, + order=1, ) self.assertTrue(self.list.listitem_set.exists()) - def mock_broadcast(_, activity, user): - """ ok """ - self.assertEqual(user.remote_id, self.local_user.remote_id) - self.assertEqual(activity["type"], "Remove") - self.assertEqual(activity["actor"], self.local_user.remote_id) - self.assertEqual(activity["target"], self.list.remote_id) - - models.ListItem.broadcast = mock_broadcast request = self.factory.post( "", { @@ -403,18 +646,15 @@ class ListViews(TestCase): ) request.user = self.local_user - views.list.remove_book(request, self.list.id) - + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.list.remove_book(request, self.list.id) self.assertFalse(self.list.listitem_set.exists()) - models.ListItem.broadcast = real_broadcast def test_remove_book_unauthorized(self): - """ take an item off a list """ + """take an item off a list""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): item = models.ListItem.objects.create( - book_list=self.list, - user=self.local_user, - book=self.book, + book_list=self.list, user=self.local_user, book=self.book, order=1 ) self.assertTrue(self.list.listitem_set.exists()) request = self.factory.post( @@ -426,5 +666,4 @@ class ListViews(TestCase): request.user = self.rat views.list.remove_book(request, self.list.id) - self.assertTrue(self.list.listitem_set.exists()) diff --git a/bookwyrm/tests/views/test_notifications.py b/bookwyrm/tests/views/test_notifications.py index 6d92485e..182753f9 100644 --- a/bookwyrm/tests/views/test_notifications.py +++ b/bookwyrm/tests/views/test_notifications.py @@ -8,10 +8,10 @@ from bookwyrm import views class NotificationViews(TestCase): - """ notifications """ + """notifications""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -23,7 +23,7 @@ class NotificationViews(TestCase): models.SiteSettings.objects.create() def test_notifications_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Notifications.as_view() request = self.factory.get("") request.user = self.local_user @@ -33,7 +33,7 @@ class NotificationViews(TestCase): self.assertEqual(result.status_code, 200) def test_clear_notifications(self): - """ erase notifications """ + """erase notifications""" models.Notification.objects.create( user=self.local_user, notification_type="FAVORITE" ) diff --git a/bookwyrm/tests/views/test_outbox.py b/bookwyrm/tests/views/test_outbox.py index 0bcfde69..f89258e5 100644 --- a/bookwyrm/tests/views/test_outbox.py +++ b/bookwyrm/tests/views/test_outbox.py @@ -13,10 +13,10 @@ from bookwyrm.settings import USER_AGENT # pylint: disable=too-many-public-methods @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class OutboxView(TestCase): - """ sends out activities """ + """sends out activities""" def setUp(self): - """ we'll need some data """ + """we'll need some data""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -34,19 +34,19 @@ class OutboxView(TestCase): ) def test_outbox(self, _): - """ returns user's statuses """ + """returns user's statuses""" request = self.factory.get("") result = views.Outbox.as_view()(request, "mouse") self.assertIsInstance(result, JsonResponse) def test_outbox_bad_method(self, _): - """ can't POST to outbox """ + """can't POST to outbox""" request = self.factory.post("") result = views.Outbox.as_view()(request, "mouse") self.assertEqual(result.status_code, 405) def test_outbox_unknown_user(self, _): - """ should 404 for unknown and remote users """ + """should 404 for unknown and remote users""" request = self.factory.post("") result = views.Outbox.as_view()(request, "beepboop") self.assertEqual(result.status_code, 405) @@ -54,7 +54,7 @@ class OutboxView(TestCase): self.assertEqual(result.status_code, 405) def test_outbox_privacy(self, _): - """ don't show dms et cetera in outbox """ + """don't show dms et cetera in outbox""" with patch("bookwyrm.activitystreams.ActivityStream.add_status"): models.Status.objects.create( content="PRIVATE!!", user=self.local_user, privacy="direct" @@ -77,7 +77,7 @@ class OutboxView(TestCase): self.assertEqual(data["totalItems"], 2) def test_outbox_filter(self, _): - """ if we only care about reviews, only get reviews """ + """if we only care about reviews, only get reviews""" with patch("bookwyrm.activitystreams.ActivityStream.add_status"): models.Review.objects.create( content="look at this", @@ -103,7 +103,7 @@ class OutboxView(TestCase): self.assertEqual(data["totalItems"], 1) def test_outbox_bookwyrm_request_true(self, _): - """ should differentiate between bookwyrm and outside requests """ + """should differentiate between bookwyrm and outside requests""" with patch("bookwyrm.activitystreams.ActivityStream.add_status"): models.Review.objects.create( name="hi", @@ -121,7 +121,7 @@ class OutboxView(TestCase): self.assertEqual(data["orderedItems"][0]["type"], "Review") def test_outbox_bookwyrm_request_false(self, _): - """ should differentiate between bookwyrm and outside requests """ + """should differentiate between bookwyrm and outside requests""" with patch("bookwyrm.activitystreams.ActivityStream.add_status"): models.Review.objects.create( name="hi", diff --git a/bookwyrm/tests/views/test_password.py b/bookwyrm/tests/views/test_password.py index 53a9bcbd..ec686db7 100644 --- a/bookwyrm/tests/views/test_password.py +++ b/bookwyrm/tests/views/test_password.py @@ -10,10 +10,10 @@ from bookwyrm import models, views class PasswordViews(TestCase): - """ view user and edit profile """ + """view user and edit profile""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -27,7 +27,7 @@ class PasswordViews(TestCase): models.SiteSettings.objects.create(id=1) def test_password_reset_request(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.PasswordResetRequest.as_view() request = self.factory.get("") request.user = self.local_user @@ -38,7 +38,7 @@ class PasswordViews(TestCase): self.assertEqual(result.status_code, 200) def test_password_reset_request_post(self): - """ send 'em an email """ + """send 'em an email""" request = self.factory.post("", {"email": "aa@bb.ccc"}) view = views.PasswordResetRequest.as_view() resp = view(request) @@ -53,7 +53,7 @@ class PasswordViews(TestCase): self.assertEqual(models.PasswordReset.objects.get().user, self.local_user) def test_password_reset(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.PasswordReset.as_view() code = models.PasswordReset.objects.create(user=self.local_user) request = self.factory.get("") @@ -64,7 +64,7 @@ class PasswordViews(TestCase): self.assertEqual(result.status_code, 200) def test_password_reset_post(self): - """ reset from code """ + """reset from code""" view = views.PasswordReset.as_view() code = models.PasswordReset.objects.create(user=self.local_user) request = self.factory.post("", {"password": "hi", "confirm-password": "hi"}) @@ -74,7 +74,7 @@ class PasswordViews(TestCase): self.assertFalse(models.PasswordReset.objects.exists()) def test_password_reset_wrong_code(self): - """ reset from code """ + """reset from code""" view = views.PasswordReset.as_view() models.PasswordReset.objects.create(user=self.local_user) request = self.factory.post("", {"password": "hi", "confirm-password": "hi"}) @@ -83,7 +83,7 @@ class PasswordViews(TestCase): self.assertTrue(models.PasswordReset.objects.exists()) def test_password_reset_mismatch(self): - """ reset from code """ + """reset from code""" view = views.PasswordReset.as_view() code = models.PasswordReset.objects.create(user=self.local_user) request = self.factory.post("", {"password": "hi", "confirm-password": "hihi"}) @@ -92,7 +92,7 @@ class PasswordViews(TestCase): self.assertTrue(models.PasswordReset.objects.exists()) def test_password_change_get(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.ChangePassword.as_view() request = self.factory.get("") request.user = self.local_user @@ -103,7 +103,7 @@ class PasswordViews(TestCase): self.assertEqual(result.status_code, 200) def test_password_change(self): - """ change password """ + """change password""" view = views.ChangePassword.as_view() password_hash = self.local_user.password request = self.factory.post("", {"password": "hi", "confirm-password": "hi"}) @@ -113,7 +113,7 @@ class PasswordViews(TestCase): self.assertNotEqual(self.local_user.password, password_hash) def test_password_change_mismatch(self): - """ change password """ + """change password""" view = views.ChangePassword.as_view() password_hash = self.local_user.password request = self.factory.post("", {"password": "hi", "confirm-password": "hihi"}) diff --git a/bookwyrm/tests/views/test_reading.py b/bookwyrm/tests/views/test_reading.py index b1ae6b88..c591aa60 100644 --- a/bookwyrm/tests/views/test_reading.py +++ b/bookwyrm/tests/views/test_reading.py @@ -10,10 +10,10 @@ from bookwyrm import models, views @patch("bookwyrm.activitystreams.ActivityStream.add_status") class ReadingViews(TestCase): - """ viewing and creating statuses """ + """viewing and creating statuses""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -41,7 +41,7 @@ class ReadingViews(TestCase): ) def test_start_reading(self, _): - """ begin a book """ + """begin a book""" shelf = self.local_user.shelf_set.get(identifier=models.Shelf.READING) self.assertFalse(shelf.books.exists()) self.assertFalse(models.Status.objects.exists()) @@ -72,7 +72,7 @@ class ReadingViews(TestCase): self.assertEqual(readthrough.book, self.book) def test_start_reading_reshelf(self, _): - """ begin a book """ + """begin a book""" to_read_shelf = self.local_user.shelf_set.get(identifier=models.Shelf.TO_READ) with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.ShelfBook.objects.create( @@ -92,7 +92,7 @@ class ReadingViews(TestCase): self.assertEqual(shelf.books.get(), self.book) def test_finish_reading(self, _): - """ begin a book """ + """begin a book""" shelf = self.local_user.shelf_set.get(identifier=models.Shelf.READ_FINISHED) self.assertFalse(shelf.books.exists()) self.assertFalse(models.Status.objects.exists()) @@ -128,7 +128,7 @@ class ReadingViews(TestCase): self.assertEqual(readthrough.book, self.book) def test_edit_readthrough(self, _): - """ adding dates to an ongoing readthrough """ + """adding dates to an ongoing readthrough""" start = timezone.make_aware(dateutil.parser.parse("2021-01-03")) readthrough = models.ReadThrough.objects.create( book=self.book, user=self.local_user, start_date=start @@ -155,7 +155,7 @@ class ReadingViews(TestCase): self.assertEqual(readthrough.book, self.book) def test_delete_readthrough(self, _): - """ remove a readthrough """ + """remove a readthrough""" readthrough = models.ReadThrough.objects.create( book=self.book, user=self.local_user ) @@ -172,7 +172,7 @@ class ReadingViews(TestCase): self.assertFalse(models.ReadThrough.objects.filter(id=readthrough.id).exists()) def test_create_readthrough(self, _): - """ adding new read dates """ + """adding new read dates""" request = self.factory.post( "", { diff --git a/bookwyrm/tests/views/test_readthrough.py b/bookwyrm/tests/views/test_readthrough.py index 5399f673..c9ebf216 100644 --- a/bookwyrm/tests/views/test_readthrough.py +++ b/bookwyrm/tests/views/test_readthrough.py @@ -9,10 +9,10 @@ from bookwyrm import models @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class ReadThrough(TestCase): - """ readthrough tests """ + """readthrough tests""" def setUp(self): - """ basic user and book data """ + """basic user and book data""" self.client = Client() self.work = models.Work.objects.create(title="Example Work") @@ -52,7 +52,7 @@ class ReadThrough(TestCase): self.assertEqual(delay_mock.call_count, 1) def test_create_progress_readthrough(self, delay_mock): - """ a readthrough with progress """ + """a readthrough with progress""" self.assertEqual(self.edition.readthrough_set.count(), 0) self.client.post( diff --git a/bookwyrm/tests/views/test_reports.py b/bookwyrm/tests/views/test_reports.py index 1c56067a..84539489 100644 --- a/bookwyrm/tests/views/test_reports.py +++ b/bookwyrm/tests/views/test_reports.py @@ -1,5 +1,4 @@ """ test for app action functionality """ -from unittest.mock import patch from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -8,10 +7,10 @@ from bookwyrm import forms, models, views class ReportViews(TestCase): - """ every response to a get request, html or json """ + """every response to a get request, html or json""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -30,7 +29,7 @@ class ReportViews(TestCase): models.SiteSettings.objects.create() def test_reports_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Reports.as_view() request = self.factory.get("") request.user = self.local_user @@ -42,7 +41,7 @@ class ReportViews(TestCase): self.assertEqual(result.status_code, 200) def test_reports_page_with_data(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Reports.as_view() request = self.factory.get("") request.user = self.local_user @@ -55,7 +54,7 @@ class ReportViews(TestCase): self.assertEqual(result.status_code, 200) def test_report_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Report.as_view() request = self.factory.get("") request.user = self.local_user @@ -69,7 +68,7 @@ class ReportViews(TestCase): self.assertEqual(result.status_code, 200) def test_report_comment(self): - """ comment on a report """ + """comment on a report""" view = views.Report.as_view() request = self.factory.post("", {"note": "hi"}) request.user = self.local_user @@ -84,7 +83,7 @@ class ReportViews(TestCase): self.assertEqual(comment.report, report) def test_make_report(self): - """ a user reports another user """ + """a user reports another user""" form = forms.ReportForm() form.data["reporter"] = self.local_user.id form.data["user"] = self.rat.id @@ -98,7 +97,7 @@ class ReportViews(TestCase): self.assertEqual(report.user, self.rat) def test_resolve_report(self): - """ toggle report resolution status """ + """toggle report resolution status""" report = models.Report.objects.create(reporter=self.local_user, user=self.rat) self.assertFalse(report.resolved) request = self.factory.post("") @@ -115,22 +114,19 @@ class ReportViews(TestCase): report.refresh_from_db() self.assertFalse(report.resolved) - def test_deactivate_user(self): - """ toggle whether a user is able to log in """ + def test_suspend_user(self): + """toggle whether a user is able to log in""" self.assertTrue(self.rat.is_active) - report = models.Report.objects.create(reporter=self.local_user, user=self.rat) request = self.factory.post("") request.user = self.local_user request.user.is_superuser = True # de-activate - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - views.deactivate_user(request, report.id) + views.suspend_user(request, self.rat.id) self.rat.refresh_from_db() self.assertFalse(self.rat.is_active) # re-activate - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - views.deactivate_user(request, report.id) + views.suspend_user(request, self.rat.id) self.rat.refresh_from_db() self.assertTrue(self.rat.is_active) diff --git a/bookwyrm/tests/views/test_rss_feed.py b/bookwyrm/tests/views/test_rss_feed.py index 0230b4a9..eacb3c93 100644 --- a/bookwyrm/tests/views/test_rss_feed.py +++ b/bookwyrm/tests/views/test_rss_feed.py @@ -8,10 +8,10 @@ from bookwyrm.views import rss_feed class RssFeedView(TestCase): - """ rss feed behaves as expected """ + """rss feed behaves as expected""" def setUp(self): - """ test data """ + """test data""" self.site = models.SiteSettings.objects.create() self.user = models.User.objects.create_user( @@ -50,7 +50,7 @@ class RssFeedView(TestCase): @patch("bookwyrm.activitystreams.ActivityStream.get_activity_stream") def test_rss_feed(self, _): - """ load an rss feed """ + """load an rss feed""" view = rss_feed.RssFeed() request = self.factory.get("/user/rss_user/rss") request.user = self.user diff --git a/bookwyrm/tests/views/test_search.py b/bookwyrm/tests/views/test_search.py index 78c7a103..77727522 100644 --- a/bookwyrm/tests/views/test_search.py +++ b/bookwyrm/tests/views/test_search.py @@ -13,10 +13,10 @@ from bookwyrm.settings import DOMAIN class ShelfViews(TestCase): - """ tag views""" + """tag views""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -38,7 +38,7 @@ class ShelfViews(TestCase): models.SiteSettings.objects.create() def test_search_json_response(self): - """ searches local data only and returns book data in json format """ + """searches local data only and returns book data in json format""" view = views.Search.as_view() # we need a connector for this, sorry request = self.factory.get("", {"q": "Test Book"}) @@ -53,11 +53,11 @@ class ShelfViews(TestCase): self.assertEqual(data[0]["key"], "https://%s/book/%d" % (DOMAIN, self.book.id)) def test_search_html_response(self): - """ searches remote connectors """ + """searches remote connectors""" view = views.Search.as_view() class TestConnector(abstract_connector.AbstractMinimalConnector): - """ nothing added here """ + """nothing added here""" def format_search_result(self, search_result): pass @@ -106,7 +106,7 @@ class ShelfViews(TestCase): ) def test_search_html_response_users(self): - """ searches remote connectors """ + """searches remote connectors""" view = views.Search.as_view() request = self.factory.get("", {"q": "mouse"}) request.user = self.local_user diff --git a/bookwyrm/tests/views/test_shelf.py b/bookwyrm/tests/views/test_shelf.py index a308fe56..239b3318 100644 --- a/bookwyrm/tests/views/test_shelf.py +++ b/bookwyrm/tests/views/test_shelf.py @@ -1,4 +1,5 @@ """ test for app action functionality """ +import json from unittest.mock import patch from django.template.response import TemplateResponse from django.test import TestCase @@ -10,10 +11,10 @@ from bookwyrm.activitypub import ActivitypubResponse @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class ShelfViews(TestCase): - """ tag views""" + """tag views""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -36,7 +37,7 @@ class ShelfViews(TestCase): models.SiteSettings.objects.create() def test_shelf_page(self, _): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Shelf.as_view() shelf = self.local_user.shelf_set.first() request = self.factory.get("") @@ -63,7 +64,7 @@ class ShelfViews(TestCase): self.assertEqual(result.status_code, 200) def test_edit_shelf_privacy(self, _): - """ set name or privacy on shelf """ + """set name or privacy on shelf""" view = views.Shelf.as_view() shelf = self.local_user.shelf_set.get(identifier="to-read") self.assertEqual(shelf.privacy, "public") @@ -83,7 +84,7 @@ class ShelfViews(TestCase): self.assertEqual(shelf.privacy, "unlisted") def test_edit_shelf_name(self, _): - """ change the name of an editable shelf """ + """change the name of an editable shelf""" view = views.Shelf.as_view() shelf = models.Shelf.objects.create(name="Test Shelf", user=self.local_user) self.assertEqual(shelf.privacy, "public") @@ -100,7 +101,7 @@ class ShelfViews(TestCase): self.assertEqual(shelf.identifier, "testshelf-%d" % shelf.id) def test_edit_shelf_name_not_editable(self, _): - """ can't change the name of an non-editable shelf """ + """can't change the name of an non-editable shelf""" view = views.Shelf.as_view() shelf = self.local_user.shelf_set.get(identifier="to-read") self.assertEqual(shelf.privacy, "public") @@ -115,18 +116,25 @@ class ShelfViews(TestCase): self.assertEqual(shelf.name, "To Read") def test_handle_shelve(self, _): - """ shelve a book """ + """shelve a book""" request = self.factory.post( "", {"book": self.book.id, "shelf": self.shelf.identifier} ) request.user = self.local_user - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: views.shelve(request) + + self.assertEqual(mock.call_count, 1) + activity = json.loads(mock.call_args[0][1]) + self.assertEqual(activity["type"], "Add") + + item = models.ShelfBook.objects.get() + self.assertEqual(activity["object"]["id"], item.remote_id) # make sure the book is on the shelf self.assertEqual(self.shelf.books.get(), self.book) def test_handle_shelve_to_read(self, _): - """ special behavior for the to-read shelf """ + """special behavior for the to-read shelf""" shelf = models.Shelf.objects.get(identifier="to-read") request = self.factory.post( "", {"book": self.book.id, "shelf": shelf.identifier} @@ -139,7 +147,7 @@ class ShelfViews(TestCase): self.assertEqual(shelf.books.get(), self.book) def test_handle_shelve_reading(self, _): - """ special behavior for the reading shelf """ + """special behavior for the reading shelf""" shelf = models.Shelf.objects.get(identifier="reading") request = self.factory.post( "", {"book": self.book.id, "shelf": shelf.identifier} @@ -152,7 +160,7 @@ class ShelfViews(TestCase): self.assertEqual(shelf.books.get(), self.book) def test_handle_shelve_read(self, _): - """ special behavior for the read shelf """ + """special behavior for the read shelf""" shelf = models.Shelf.objects.get(identifier="read") request = self.factory.post( "", {"book": self.book.id, "shelf": shelf.identifier} @@ -165,15 +173,20 @@ class ShelfViews(TestCase): self.assertEqual(shelf.books.get(), self.book) def test_handle_unshelve(self, _): - """ remove a book from a shelf """ + """remove a book from a shelf""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.ShelfBook.objects.create( book=self.book, user=self.local_user, shelf=self.shelf ) + item = models.ShelfBook.objects.get() + self.shelf.save() self.assertEqual(self.shelf.books.count(), 1) request = self.factory.post("", {"book": self.book.id, "shelf": self.shelf.id}) request.user = self.local_user - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: views.unshelve(request) + activity = json.loads(mock.call_args[0][1]) + self.assertEqual(activity["type"], "Remove") + self.assertEqual(activity["object"]["id"], item.remote_id) self.assertEqual(self.shelf.books.count(), 0) diff --git a/bookwyrm/tests/views/test_status.py b/bookwyrm/tests/views/test_status.py index e7fc62d5..6f2fd30d 100644 --- a/bookwyrm/tests/views/test_status.py +++ b/bookwyrm/tests/views/test_status.py @@ -10,10 +10,10 @@ from bookwyrm.settings import DOMAIN @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class StatusViews(TestCase): - """ viewing and creating statuses """ + """viewing and creating statuses""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -43,7 +43,7 @@ class StatusViews(TestCase): models.SiteSettings.objects.create() def test_handle_status(self, _): - """ create a status """ + """create a status""" view = views.CreateStatus.as_view() form = forms.CommentForm( { @@ -66,7 +66,7 @@ class StatusViews(TestCase): self.assertEqual(status.book, self.book) def test_handle_status_reply(self, _): - """ create a status in reply to an existing status """ + """create a status in reply to an existing status""" view = views.CreateStatus.as_view() user = models.User.objects.create_user( "rat", "rat@rat.com", "password", local=True @@ -96,7 +96,7 @@ class StatusViews(TestCase): self.assertEqual(models.Notification.objects.get().user, self.local_user) def test_handle_status_mentions(self, _): - """ @mention a user in a post """ + """@mention a user in a post""" view = views.CreateStatus.as_view() user = models.User.objects.create_user( "rat@%s" % DOMAIN, "rat@rat.com", "password", local=True, localname="rat" @@ -124,7 +124,7 @@ class StatusViews(TestCase): ) def test_handle_status_reply_with_mentions(self, _): - """ reply to a post with an @mention'ed user """ + """reply to a post with an @mention'ed user""" view = views.CreateStatus.as_view() user = models.User.objects.create_user( "rat", "rat@rat.com", "password", local=True, localname="rat" @@ -168,7 +168,7 @@ class StatusViews(TestCase): self.assertTrue(self.local_user in reply.mention_users.all()) def test_delete_and_redraft(self, _): - """ delete and re-draft a status """ + """delete and re-draft a status""" view = views.DeleteAndRedraft.as_view() request = self.factory.post("") request.user = self.local_user @@ -177,7 +177,9 @@ class StatusViews(TestCase): content="hi", book=self.book, user=self.local_user ) - with patch("bookwyrm.activitystreams.ActivityStream.remove_status") as mock: + with patch( + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" + ) as mock: result = view(request, status.id) self.assertTrue(mock.called) result.render() @@ -187,7 +189,7 @@ class StatusViews(TestCase): self.assertTrue(status.deleted) def test_delete_and_redraft_invalid_status_type_rating(self, _): - """ you can't redraft generated statuses """ + """you can't redraft generated statuses""" view = views.DeleteAndRedraft.as_view() request = self.factory.post("") request.user = self.local_user @@ -196,7 +198,9 @@ class StatusViews(TestCase): book=self.book, rating=2.0, user=self.local_user ) - with patch("bookwyrm.activitystreams.ActivityStream.remove_status") as mock: + with patch( + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" + ) as mock: result = view(request, status.id) self.assertFalse(mock.called) self.assertEqual(result.status_code, 400) @@ -205,7 +209,7 @@ class StatusViews(TestCase): self.assertFalse(status.deleted) def test_delete_and_redraft_invalid_status_type_generated_note(self, _): - """ you can't redraft generated statuses """ + """you can't redraft generated statuses""" view = views.DeleteAndRedraft.as_view() request = self.factory.post("") request.user = self.local_user @@ -214,7 +218,9 @@ class StatusViews(TestCase): content="hi", user=self.local_user ) - with patch("bookwyrm.activitystreams.ActivityStream.remove_status") as mock: + with patch( + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" + ) as mock: result = view(request, status.id) self.assertFalse(mock.called) self.assertEqual(result.status_code, 400) @@ -223,7 +229,7 @@ class StatusViews(TestCase): self.assertFalse(status.deleted) def test_find_mentions(self, _): - """ detect and look up @ mentions of users """ + """detect and look up @ mentions of users""" user = models.User.objects.create_user( "nutria@%s" % DOMAIN, "nutria@nutria.com", @@ -269,7 +275,7 @@ class StatusViews(TestCase): ) def test_format_links(self, _): - """ find and format urls into a tags """ + """find and format urls into a tags""" url = "http://www.fish.com/" self.assertEqual( views.status.format_links(url), 'www.fish.com/' % url @@ -292,7 +298,7 @@ class StatusViews(TestCase): ) def test_to_markdown(self, _): - """ this is mostly handled in other places, but nonetheless """ + """this is mostly handled in other places, but nonetheless""" text = "_hi_ and http://fish.com is rad" result = views.status.to_markdown(text) self.assertEqual( @@ -301,13 +307,13 @@ class StatusViews(TestCase): ) def test_to_markdown_link(self, _): - """ this is mostly handled in other places, but nonetheless """ + """this is mostly handled in other places, but nonetheless""" text = "[hi](http://fish.com) is rad" result = views.status.to_markdown(text) self.assertEqual(result, '

    hi ' "is rad

    ") def test_handle_delete_status(self, mock): - """ marks a status as deleted """ + """marks a status as deleted""" view = views.DeleteStatus.as_view() with patch("bookwyrm.activitystreams.ActivityStream.add_status"): status = models.Status.objects.create(user=self.local_user, content="hi") @@ -316,7 +322,7 @@ class StatusViews(TestCase): request.user = self.local_user with patch( - "bookwyrm.activitystreams.ActivityStream.remove_status" + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" ) as redis_mock: view(request, status.id) self.assertTrue(redis_mock.called) @@ -327,7 +333,7 @@ class StatusViews(TestCase): self.assertTrue(status.deleted) def test_handle_delete_status_permission_denied(self, _): - """ marks a status as deleted """ + """marks a status as deleted""" view = views.DeleteStatus.as_view() with patch("bookwyrm.activitystreams.ActivityStream.add_status"): status = models.Status.objects.create(user=self.local_user, content="hi") @@ -341,7 +347,7 @@ class StatusViews(TestCase): self.assertFalse(status.deleted) def test_handle_delete_status_moderator(self, mock): - """ marks a status as deleted """ + """marks a status as deleted""" view = views.DeleteStatus.as_view() with patch("bookwyrm.activitystreams.ActivityStream.add_status"): status = models.Status.objects.create(user=self.local_user, content="hi") @@ -351,7 +357,7 @@ class StatusViews(TestCase): request.user.is_superuser = True with patch( - "bookwyrm.activitystreams.ActivityStream.remove_status" + "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" ) as redis_mock: view(request, status.id) self.assertTrue(redis_mock.called) diff --git a/bookwyrm/tests/views/test_tag.py b/bookwyrm/tests/views/test_tag.py deleted file mode 100644 index 6ad6ab25..00000000 --- a/bookwyrm/tests/views/test_tag.py +++ /dev/null @@ -1,119 +0,0 @@ -""" test for app action functionality """ -from unittest.mock import patch -from django.contrib.auth.models import Group, Permission -from django.contrib.contenttypes.models import ContentType -from django.template.response import TemplateResponse -from django.test import TestCase -from django.test.client import RequestFactory - -from bookwyrm import models, views -from bookwyrm.activitypub import ActivitypubResponse - - -class TagViews(TestCase): - """ tag views""" - - def setUp(self): - """ we need basic test data and mocks """ - self.factory = RequestFactory() - self.local_user = models.User.objects.create_user( - "mouse@local.com", - "mouse@mouse.com", - "mouseword", - local=True, - localname="mouse", - remote_id="https://example.com/users/mouse", - ) - self.group = Group.objects.create(name="editor") - self.group.permissions.add( - Permission.objects.create( - name="edit_book", - codename="edit_book", - content_type=ContentType.objects.get_for_model(models.User), - ).id - ) - self.work = models.Work.objects.create(title="Test Work") - self.book = models.Edition.objects.create( - title="Example Edition", - remote_id="https://example.com/book/1", - parent_work=self.work, - ) - models.SiteSettings.objects.create() - - def test_tag_page(self): - """ there are so many views, this just makes sure it LOADS """ - view = views.Tag.as_view() - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - tag = models.Tag.objects.create(name="hi there") - models.UserTag.objects.create(tag=tag, user=self.local_user, book=self.book) - request = self.factory.get("") - with patch("bookwyrm.views.tag.is_api_request") as is_api: - is_api.return_value = False - result = view(request, tag.identifier) - self.assertIsInstance(result, TemplateResponse) - result.render() - self.assertEqual(result.status_code, 200) - - request = self.factory.get("") - with patch("bookwyrm.views.tag.is_api_request") as is_api: - is_api.return_value = True - result = view(request, tag.identifier) - self.assertIsInstance(result, ActivitypubResponse) - self.assertEqual(result.status_code, 200) - - def test_tag_page_activitypub_page(self): - """ there are so many views, this just makes sure it LOADS """ - view = views.Tag.as_view() - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - tag = models.Tag.objects.create(name="hi there") - models.UserTag.objects.create(tag=tag, user=self.local_user, book=self.book) - request = self.factory.get("", {"page": 1}) - with patch("bookwyrm.views.tag.is_api_request") as is_api: - is_api.return_value = True - result = view(request, tag.identifier) - self.assertIsInstance(result, ActivitypubResponse) - self.assertEqual(result.status_code, 200) - - def test_tag(self): - """ add a tag to a book """ - view = views.AddTag.as_view() - request = self.factory.post( - "", - { - "name": "A Tag!?", - "book": self.book.id, - }, - ) - request.user = self.local_user - - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - view(request) - - tag = models.Tag.objects.get() - user_tag = models.UserTag.objects.get() - self.assertEqual(tag.name, "A Tag!?") - self.assertEqual(tag.identifier, "A+Tag%21%3F") - self.assertEqual(user_tag.user, self.local_user) - self.assertEqual(user_tag.book, self.book) - - def test_untag(self): - """ remove a tag from a book """ - view = views.RemoveTag.as_view() - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - tag = models.Tag.objects.create(name="A Tag!?") - models.UserTag.objects.create(user=self.local_user, book=self.book, tag=tag) - request = self.factory.post( - "", - { - "user": self.local_user.id, - "book": self.book.id, - "name": tag.name, - }, - ) - request.user = self.local_user - - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - view(request) - - self.assertTrue(models.Tag.objects.filter(name="A Tag!?").exists()) - self.assertFalse(models.UserTag.objects.exists()) diff --git a/bookwyrm/tests/views/test_updates.py b/bookwyrm/tests/views/test_updates.py index dff730e6..fb003f8d 100644 --- a/bookwyrm/tests/views/test_updates.py +++ b/bookwyrm/tests/views/test_updates.py @@ -10,10 +10,10 @@ from bookwyrm import models, views class UpdateViews(TestCase): - """ lets the ui check for unread notification """ + """lets the ui check for unread notification""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -25,7 +25,7 @@ class UpdateViews(TestCase): models.SiteSettings.objects.create() def test_get_notification_count(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" request = self.factory.get("") request.user = self.local_user @@ -43,7 +43,7 @@ class UpdateViews(TestCase): self.assertEqual(data["count"], 1) def test_get_unread_status_count(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" request = self.factory.get("") request.user = self.local_user diff --git a/bookwyrm/tests/views/test_user.py b/bookwyrm/tests/views/test_user.py index 055edae2..3b431de1 100644 --- a/bookwyrm/tests/views/test_user.py +++ b/bookwyrm/tests/views/test_user.py @@ -15,10 +15,10 @@ from bookwyrm.activitypub import ActivitypubResponse class UserViews(TestCase): - """ view user and edit profile """ + """view user and edit profile""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -30,12 +30,20 @@ class UserViews(TestCase): self.rat = models.User.objects.create_user( "rat@local.com", "rat@rat.rat", "password", local=True, localname="rat" ) + self.book = models.Edition.objects.create(title="test") + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + models.ShelfBook.objects.create( + book=self.book, + user=self.local_user, + shelf=self.local_user.shelf_set.first(), + ) + models.SiteSettings.objects.create() self.anonymous_user = AnonymousUser self.anonymous_user.is_authenticated = False def test_user_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.User.as_view() request = self.factory.get("") request.user = self.local_user @@ -61,7 +69,7 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 200) def test_user_page_blocked(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.User.as_view() request = self.factory.get("") request.user = self.local_user @@ -72,7 +80,7 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 404) def test_followers_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Followers.as_view() request = self.factory.get("") request.user = self.local_user @@ -90,7 +98,7 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 200) def test_followers_page_blocked(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Followers.as_view() request = self.factory.get("") request.user = self.local_user @@ -101,7 +109,7 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 404) def test_following_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Following.as_view() request = self.factory.get("") request.user = self.local_user @@ -119,7 +127,7 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 200) def test_following_page_blocked(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Following.as_view() request = self.factory.get("") request.user = self.local_user @@ -130,7 +138,7 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 404) def test_edit_user_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.EditUser.as_view() request = self.factory.get("") request.user = self.local_user @@ -140,7 +148,7 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 200) def test_edit_user(self): - """ use a form to update a user """ + """use a form to update a user""" view = views.EditUser.as_view() form = forms.EditUserForm(instance=self.local_user) form.data["name"] = "New Name" @@ -160,7 +168,7 @@ class UserViews(TestCase): # idk how to mock the upload form, got tired of triyng to make it work def test_edit_user_avatar(self): - """ use a form to update a user """ + """use a form to update a user""" view = views.EditUser.as_view() form = forms.EditUserForm(instance=self.local_user) form.data["name"] = "New Name" @@ -187,7 +195,7 @@ class UserViews(TestCase): self.assertEqual(self.local_user.avatar.height, 120) def test_crop_avatar(self): - """ reduce that image size """ + """reduce that image size""" image_file = pathlib.Path(__file__).parent.joinpath( "../../static/images/no_cover.jpg" ) diff --git a/bookwyrm/tests/views/test_user_admin.py b/bookwyrm/tests/views/test_user_admin.py index dd20c1b6..a044a22c 100644 --- a/bookwyrm/tests/views/test_user_admin.py +++ b/bookwyrm/tests/views/test_user_admin.py @@ -1,4 +1,6 @@ """ test for app action functionality """ +from unittest.mock import patch +from django.contrib.auth.models import Group from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -7,10 +9,10 @@ from bookwyrm import models, views class UserAdminViews(TestCase): - """ every response to a get request, html or json """ + """every response to a get request, html or json""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -21,9 +23,9 @@ class UserAdminViews(TestCase): ) models.SiteSettings.objects.create() - def test_user_admin_page(self): - """ there are so many views, this just makes sure it LOADS """ - view = views.UserAdmin.as_view() + def test_user_admin_list_page(self): + """there are so many views, this just makes sure it LOADS""" + view = views.UserAdminList.as_view() request = self.factory.get("") request.user = self.local_user request.user.is_superuser = True @@ -31,3 +33,38 @@ class UserAdminViews(TestCase): self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) + + def test_user_admin_page(self): + """there are so many views, this just makes sure it LOADS""" + view = views.UserAdmin.as_view() + request = self.factory.get("") + request.user = self.local_user + request.user.is_superuser = True + + result = view(request, self.local_user.id) + + self.assertIsInstance(result, TemplateResponse) + result.render() + self.assertEqual(result.status_code, 200) + + def test_user_admin_page_post(self): + """set the user's group""" + group = Group.objects.create(name="editor") + self.assertEqual( + list(self.local_user.groups.values_list("name", flat=True)), [] + ) + + view = views.UserAdmin.as_view() + request = self.factory.post("", {"groups": [group.id]}) + request.user = self.local_user + request.user.is_superuser = True + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + result = view(request, self.local_user.id) + + self.assertIsInstance(result, TemplateResponse) + result.render() + + self.assertEqual( + list(self.local_user.groups.values_list("name", flat=True)), ["editor"] + ) diff --git a/bookwyrm/tests/views/test_wellknown.py b/bookwyrm/tests/views/test_wellknown.py index f408460f..244921d4 100644 --- a/bookwyrm/tests/views/test_wellknown.py +++ b/bookwyrm/tests/views/test_wellknown.py @@ -11,10 +11,10 @@ from bookwyrm import models, views class UserViews(TestCase): - """ view user and edit profile """ + """view user and edit profile""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -41,7 +41,7 @@ class UserViews(TestCase): self.anonymous_user.is_authenticated = False def test_webfinger(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" request = self.factory.get("", {"resource": "acct:mouse@local.com"}) request.user = self.anonymous_user @@ -51,7 +51,7 @@ class UserViews(TestCase): self.assertEqual(data["subject"], "acct:mouse@local.com") def test_nodeinfo_pointer(self): - """ just tells you where nodeinfo is """ + """just tells you where nodeinfo is""" request = self.factory.get("") request.user = self.anonymous_user @@ -61,7 +61,7 @@ class UserViews(TestCase): self.assertTrue("href" in data["links"][0]) def test_nodeinfo(self): - """ info about the instance """ + """info about the instance""" request = self.factory.get("") request.user = self.anonymous_user @@ -73,7 +73,7 @@ class UserViews(TestCase): self.assertEqual(models.User.objects.count(), 3) def test_instanceinfo(self): - """ about the instance's user activity """ + """about the instance's user activity""" request = self.factory.get("") request.user = self.anonymous_user diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 46398806..53ceeaa8 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -51,13 +51,20 @@ urlpatterns = [ r"^password-reset/(?P[A-Za-z0-9]+)/?$", views.PasswordReset.as_view() ), # admin - re_path(r"^settings/site-settings", views.Site.as_view(), name="settings-site"), + re_path(r"^settings/site-settings/?$", views.Site.as_view(), name="settings-site"), re_path( - r"^settings/email-preview", + r"^settings/email-preview/?$", views.site.email_preview, name="settings-email-preview", ), - re_path(r"^settings/users", views.UserAdmin.as_view(), name="settings-users"), + re_path( + r"^settings/users/?$", views.UserAdminList.as_view(), name="settings-users" + ), + re_path( + r"^settings/users/(?P\d+)/?$", + views.UserAdmin.as_view(), + name="settings-user", + ), re_path( r"^settings/federation/?$", views.Federation.as_view(), @@ -68,6 +75,26 @@ urlpatterns = [ views.FederatedServer.as_view(), name="settings-federated-server", ), + re_path( + r"^settings/federation/(?P\d+)/block?$", + views.federation.block_server, + name="settings-federated-server-block", + ), + re_path( + r"^settings/federation/(?P\d+)/unblock?$", + views.federation.unblock_server, + name="settings-federated-server-unblock", + ), + re_path( + r"^settings/federation/add/?$", + views.AddFederatedServer.as_view(), + name="settings-add-federated-server", + ), + re_path( + r"^settings/federation/import/?$", + views.ImportServerBlocklist.as_view(), + name="settings-import-blocklist", + ), re_path( r"^settings/invites/?$", views.ManageInvites.as_view(), name="settings-invites" ), @@ -93,9 +120,9 @@ urlpatterns = [ name="settings-report", ), re_path( - r"^settings/reports/(?P\d+)/deactivate/?$", - views.deactivate_user, - name="settings-report-deactivate", + r"^settings/reports/(?P\d+)/suspend/?$", + views.suspend_user, + name="settings-report-suspend", ), re_path( r"^settings/reports/(?P\d+)/resolve/?$", @@ -164,6 +191,11 @@ urlpatterns = [ views.list.remove_book, name="list-remove-book", ), + re_path( + r"^list-item/(?P\d+)/set-position$", + views.list.set_book_position, + name="list-set-book-position", + ), re_path( r"^list/(?P\d+)/curate/?$", views.Curate.as_view(), name="list-curate" ), @@ -228,7 +260,12 @@ urlpatterns = [ re_path(r"^boost/(?P\d+)/?$", views.Boost.as_view()), re_path(r"^unboost/(?P\d+)/?$", views.Unboost.as_view()), # books - re_path(r"%s(.json)?/?$" % book_path, views.Book.as_view()), + re_path(r"%s(.json)?/?$" % book_path, views.Book.as_view(), name="book"), + re_path( + r"%s/(?Preview|comment|quote)/?$" % book_path, + views.Book.as_view(), + name="book-user-statuses", + ), re_path(r"%s/edit/?$" % book_path, views.EditBook.as_view()), re_path(r"%s/confirm/?$" % book_path, views.ConfirmEditBook.as_view()), re_path(r"^create-book/?$", views.EditBook.as_view()), @@ -245,11 +282,6 @@ urlpatterns = [ # author re_path(r"^author/(?P\d+)(.json)?/?$", views.Author.as_view()), re_path(r"^author/(?P\d+)/edit/?$", views.EditAuthor.as_view()), - # tags - re_path(r"^tag/(?P.+)\.json/?$", views.Tag.as_view()), - re_path(r"^tag/(?P.+)/?$", views.Tag.as_view()), - re_path(r"^tag/?$", views.AddTag.as_view()), - re_path(r"^untag/?$", views.RemoveTag.as_view()), # reading progress re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"), re_path(r"^delete-readthrough/?$", views.delete_readthrough), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index d053e971..bcd914e1 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -6,6 +6,8 @@ from .books import Book, EditBook, ConfirmEditBook, Editions from .books import upload_cover, add_description, switch_edition, resolve_book from .directory import Directory from .federation import Federation, FederatedServer +from .federation import AddFederatedServer, ImportServerBlocklist +from .federation import block_server, unblock_server from .feed import DirectMessage, Feed, Replies, Status from .follow import follow, unfollow from .follow import accept_follow_request, delete_follow_request @@ -23,7 +25,7 @@ from .notifications import Notifications from .outbox import Outbox from .reading import edit_readthrough, create_readthrough, delete_readthrough from .reading import start_reading, finish_reading, delete_progressupdate -from .reports import Report, Reports, make_report, resolve_report, deactivate_user +from .reports import Report, Reports, make_report, resolve_report, suspend_user from .rss_feed import RssFeed from .password import PasswordResetRequest, PasswordReset, ChangePassword from .search import Search @@ -32,8 +34,7 @@ from .shelf import create_shelf, delete_shelf from .shelf import shelve, unshelve from .site import Site from .status import CreateStatus, DeleteStatus, DeleteAndRedraft -from .tag import Tag, AddTag, RemoveTag from .updates import get_notification_count, get_unread_status_count from .user import User, EditUser, Followers, Following -from .user_admin import UserAdmin +from .user_admin import UserAdmin, UserAdminList from .wellknown import * diff --git a/bookwyrm/views/authentication.py b/bookwyrm/views/authentication.py index 22689a28..bfb49248 100644 --- a/bookwyrm/views/authentication.py +++ b/bookwyrm/views/authentication.py @@ -16,10 +16,10 @@ from bookwyrm.settings import DOMAIN # pylint: disable= no-self-use @method_decorator(csrf_exempt, name="dispatch") class Login(View): - """ authenticate an existing user """ + """authenticate an existing user""" def get(self, request): - """ login page """ + """login page""" if request.user.is_authenticated: return redirect("/") # sene user to the login page @@ -30,7 +30,7 @@ class Login(View): return TemplateResponse(request, "login.html", data) def post(self, request): - """ authentication action """ + """authentication action""" if request.user.is_authenticated: return redirect("/") login_form = forms.LoginForm(request.POST) @@ -61,10 +61,10 @@ class Login(View): class Register(View): - """ register a user """ + """register a user""" def post(self, request): - """ join the server """ + """join the server""" if not models.SiteSettings.get().allow_registration: invite_code = request.POST.get("invite_code") @@ -117,9 +117,9 @@ class Register(View): @method_decorator(login_required, name="dispatch") class Logout(View): - """ log out """ + """log out""" def get(self, request): - """ done with this place! outa here! """ + """done with this place! outa here!""" logout(request) return redirect("/") diff --git a/bookwyrm/views/author.py b/bookwyrm/views/author.py index 50a3588d..0bd7b0e0 100644 --- a/bookwyrm/views/author.py +++ b/bookwyrm/views/author.py @@ -13,10 +13,10 @@ from .helpers import is_api_request # pylint: disable= no-self-use class Author(View): - """ this person wrote a book """ + """this person wrote a book""" def get(self, request, author_id): - """ landing page for an author """ + """landing page for an author""" author = get_object_or_404(models.Author, id=author_id) if is_api_request(request): @@ -37,16 +37,16 @@ class Author(View): permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch" ) class EditAuthor(View): - """ edit author info """ + """edit author info""" def get(self, request, author_id): - """ info about a book """ + """info about a book""" author = get_object_or_404(models.Author, id=author_id) data = {"author": author, "form": forms.AuthorForm(instance=author)} return TemplateResponse(request, "edit_author.html", data) def post(self, request, author_id): - """ edit a author cool """ + """edit a author cool""" author = get_object_or_404(models.Author, id=author_id) form = forms.AuthorForm(request.POST, request.FILES, instance=author) diff --git a/bookwyrm/views/block.py b/bookwyrm/views/block.py index 6d6a8a58..99014a93 100644 --- a/bookwyrm/views/block.py +++ b/bookwyrm/views/block.py @@ -12,14 +12,14 @@ from bookwyrm import models # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class Block(View): - """ blocking users """ + """blocking users""" def get(self, request): - """ list of blocked users? """ + """list of blocked users?""" return TemplateResponse(request, "preferences/blocks.html") def post(self, request, user_id): - """ block a user """ + """block a user""" to_block = get_object_or_404(models.User, id=user_id) models.UserBlocks.objects.create( user_subject=request.user, user_object=to_block @@ -30,7 +30,7 @@ class Block(View): @require_POST @login_required def unblock(request, user_id): - """ undo a block """ + """undo a block""" to_unblock = get_object_or_404(models.User, id=user_id) try: block = models.UserBlocks.objects.get( diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index 58886cad..448cf992 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -1,6 +1,7 @@ """ the good stuff! the books! """ from uuid import uuid4 +from dateutil.parser import parse as dateparse from django.contrib.auth.decorators import login_required, permission_required from django.contrib.postgres.search import SearchRank, SearchVector from django.core.files.base import ContentFile @@ -10,6 +11,7 @@ from django.db.models import Avg, Q from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse +from django.utils.datastructures import MultiValueDictKeyError from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.http import require_POST @@ -24,15 +26,10 @@ from .helpers import is_api_request, get_edition, privacy_filter # pylint: disable= no-self-use class Book(View): - """ a book! this is the stuff """ - - def get(self, request, book_id): - """ info about a book """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 + """a book! this is the stuff""" + def get(self, request, book_id, user_statuses=False): + """info about a book""" try: book = models.Book.objects.select_subclasses().get(id=book_id) except models.Book.DoesNotExist: @@ -43,29 +40,41 @@ class Book(View): if isinstance(book, models.Work): book = book.get_default_edition() - if not book: + if not book or not book.parent_work: return HttpResponseNotFound() work = book.parent_work - if not work: - return HttpResponseNotFound() # all reviews for the book - reviews = models.Review.objects.filter(book__in=work.editions.all()) - reviews = privacy_filter(request.user, reviews) + reviews = privacy_filter( + request.user, models.Review.objects.filter(book__in=work.editions.all()) + ) # the reviews to show - paginated = Paginator( - reviews.exclude(Q(content__isnull=True) | Q(content="")), PAGE_LENGTH - ) - reviews_page = paginated.page(page) + if user_statuses and request.user.is_authenticated: + if user_statuses == "review": + queryset = book.review_set + elif user_statuses == "comment": + queryset = book.comment_set + else: + queryset = book.quotation_set + queryset = queryset.filter(user=request.user) + else: + queryset = reviews.exclude(Q(content__isnull=True) | Q(content="")) + paginated = Paginator(queryset, PAGE_LENGTH) + + data = { + "book": book, + "statuses": paginated.get_page(request.GET.get("page")), + "review_count": reviews.count(), + "ratings": reviews.filter(Q(content__isnull=True) | Q(content="")), + "rating": reviews.aggregate(Avg("rating"))["rating__avg"], + "lists": privacy_filter( + request.user, book.list_set.filter(listitem__approved=True) + ), + } - user_tags = readthroughs = user_shelves = other_edition_shelves = [] if request.user.is_authenticated: - user_tags = models.UserTag.objects.filter( - book=book, user=request.user - ).values_list("tag__identifier", flat=True) - readthroughs = models.ReadThrough.objects.filter( user=request.user, book=book, @@ -75,31 +84,24 @@ class Book(View): readthrough.progress_updates = ( readthrough.progressupdate_set.all().order_by("-updated_date") ) + data["readthroughs"] = readthroughs - user_shelves = models.ShelfBook.objects.filter(user=request.user, book=book) + data["user_shelves"] = models.ShelfBook.objects.filter( + user=request.user, book=book + ) - other_edition_shelves = models.ShelfBook.objects.filter( + data["other_edition_shelves"] = models.ShelfBook.objects.filter( ~Q(book=book), user=request.user, book__parent_work=book.parent_work, ) - data = { - "book": book, - "reviews": reviews_page, - "review_count": reviews.count(), - "ratings": reviews.filter(Q(content__isnull=True) | Q(content="")), - "rating": reviews.aggregate(Avg("rating"))["rating__avg"], - "tags": models.UserTag.objects.filter(book=book), - "lists": privacy_filter( - request.user, book.list_set.filter(listitem__approved=True) - ), - "user_tags": user_tags, - "user_shelves": user_shelves, - "other_edition_shelves": other_edition_shelves, - "readthroughs": readthroughs, - "path": "/book/%s" % book_id, - } + data["user_statuses"] = { + "review_count": book.review_set.filter(user=request.user).count(), + "comment_count": book.comment_set.filter(user=request.user).count(), + "quotation_count": book.quotation_set.filter(user=request.user).count(), + } + return TemplateResponse(request, "book/book.html", data) @@ -108,10 +110,10 @@ class Book(View): permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch" ) class EditBook(View): - """ edit a book """ + """edit a book""" def get(self, request, book_id=None): - """ info about a book """ + """info about a book""" book = None if book_id: book = get_edition(book_id) @@ -121,7 +123,7 @@ class EditBook(View): return TemplateResponse(request, "book/edit_book.html", data) def post(self, request, book_id=None): - """ edit a book cool """ + """edit a book cool""" # returns None if no match is found book = models.Edition.objects.filter(id=book_id).first() form = forms.EditionForm(request.POST, request.FILES, instance=book) @@ -172,6 +174,20 @@ class EditBook(View): data["confirm_mode"] = True # this isn't preserved because it isn't part of the form obj data["remove_authors"] = request.POST.getlist("remove_authors") + # make sure the dates are passed in as datetime, they're currently a string + # QueryDicts are immutable, we need to copy + formcopy = data["form"].data.copy() + try: + formcopy["first_published_date"] = dateparse( + formcopy["first_published_date"] + ) + except (MultiValueDictKeyError, ValueError): + pass + try: + formcopy["published_date"] = dateparse(formcopy["published_date"]) + except (MultiValueDictKeyError, ValueError): + pass + data["form"].data = formcopy return TemplateResponse(request, "book/edit_book.html", data) remove_authors = request.POST.getlist("remove_authors") @@ -193,10 +209,10 @@ class EditBook(View): permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch" ) class ConfirmEditBook(View): - """ confirm edits to a book """ + """confirm edits to a book""" def post(self, request, book_id=None): - """ edit a book cool """ + """edit a book cool""" # returns None if no match is found book = models.Edition.objects.filter(id=book_id).first() form = forms.EditionForm(request.POST, request.FILES, instance=book) @@ -244,17 +260,12 @@ class ConfirmEditBook(View): class Editions(View): - """ list of editions """ + """list of editions""" def get(self, request, book_id): - """ list of editions of a book """ + """list of editions of a book""" work = get_object_or_404(models.Work, id=book_id) - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - if is_api_request(request): return ActivitypubResponse(work.to_edition_list(**request.GET)) filters = {} @@ -264,12 +275,12 @@ class Editions(View): if request.GET.get("format"): filters["physical_format__iexact"] = request.GET.get("format") - editions = work.editions.order_by("-edition_rank").all() + editions = work.editions.order_by("-edition_rank") languages = set(sum([e.languages for e in editions], [])) - paginated = Paginator(editions.filter(**filters).all(), PAGE_LENGTH) + paginated = Paginator(editions.filter(**filters), PAGE_LENGTH) data = { - "editions": paginated.page(page), + "editions": paginated.get_page(request.GET.get("page")), "work": work, "languages": languages, "formats": set( @@ -282,7 +293,7 @@ class Editions(View): @login_required @require_POST def upload_cover(request, book_id): - """ upload a new cover """ + """upload a new cover""" book = get_object_or_404(models.Edition, id=book_id) book.last_edited_by = request.user @@ -305,7 +316,7 @@ def upload_cover(request, book_id): def set_cover_from_url(url): - """ load it from a url """ + """load it from a url""" image_file = get_image(url) if not image_file: return None @@ -318,7 +329,7 @@ def set_cover_from_url(url): @require_POST @permission_required("bookwyrm.edit_book", raise_exception=True) def add_description(request, book_id): - """ upload a new cover """ + """upload a new cover""" if not request.method == "POST": return redirect("/") @@ -335,7 +346,7 @@ def add_description(request, book_id): @require_POST def resolve_book(request): - """ figure out the local path to a book from a remote_id """ + """figure out the local path to a book from a remote_id""" remote_id = request.POST.get("remote_id") connector = connector_manager.get_or_create_connector(remote_id) book = connector.get_or_create_book(remote_id) @@ -347,7 +358,7 @@ def resolve_book(request): @require_POST @transaction.atomic def switch_edition(request): - """ switch your copy of a book to a different edition """ + """switch your copy of a book to a different edition""" edition_id = request.POST.get("edition") new_edition = get_object_or_404(models.Edition, id=edition_id) shelfbooks = models.ShelfBook.objects.filter( diff --git a/bookwyrm/views/directory.py b/bookwyrm/views/directory.py index 9504734e..a5786f74 100644 --- a/bookwyrm/views/directory.py +++ b/bookwyrm/views/directory.py @@ -11,16 +11,10 @@ from .helpers import get_annotated_users # pylint: disable=no-self-use @method_decorator(login_required, name="dispatch") class Directory(View): - """ display of known bookwyrm users """ + """display of known bookwyrm users""" def get(self, request): - """ lets see your cute faces """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - - # filters + """lets see your cute faces""" filters = {} software = request.GET.get("software") if not software or software == "bookwyrm": @@ -39,12 +33,12 @@ class Directory(View): paginated = Paginator(users, 12) data = { - "users": paginated.page(page), + "users": paginated.get_page(request.GET.get("page")), } return TemplateResponse(request, "directory/directory.html", data) def post(self, request): - """ join the directory """ + """join the directory""" request.user.discoverable = True request.user.save() return redirect("directory") diff --git a/bookwyrm/views/federation.py b/bookwyrm/views/federation.py index 464a207c..d4a1af12 100644 --- a/bookwyrm/views/federation.py +++ b/bookwyrm/views/federation.py @@ -1,12 +1,15 @@ """ manage federated servers """ +import json from django.contrib.auth.decorators import login_required, permission_required from django.core.paginator import Paginator -from django.shortcuts import get_object_or_404 +from django.db import transaction +from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.utils.decorators import method_decorator from django.views import View +from django.views.decorators.http import require_POST -from bookwyrm import models +from bookwyrm import forms, models from bookwyrm.settings import PAGE_LENGTH @@ -17,37 +20,92 @@ from bookwyrm.settings import PAGE_LENGTH name="dispatch", ) class Federation(View): - """ what servers do we federate with """ + """what servers do we federate with""" def get(self, request): - """ list of servers """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - - servers = models.FederatedServer.objects.all() + """list of servers""" + servers = models.FederatedServer.objects sort = request.GET.get("sort") sort_fields = ["created_date", "application_type", "server_name"] - if sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]: - servers = servers.order_by(sort) + if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]: + sort = "created_date" + servers = servers.order_by(sort) paginated = Paginator(servers, PAGE_LENGTH) - data = {"servers": paginated.page(page), "sort": sort} + + data = { + "servers": paginated.get_page(request.GET.get("page")), + "sort": sort, + "form": forms.ServerForm(), + } return TemplateResponse(request, "settings/federation.html", data) +class AddFederatedServer(View): + """manually add a server""" + + def get(self, request): + """add server form""" + data = {"form": forms.ServerForm()} + return TemplateResponse(request, "settings/edit_server.html", data) + + def post(self, request): + """add a server from the admin panel""" + form = forms.ServerForm(request.POST) + if not form.is_valid(): + data = {"form": form} + return TemplateResponse(request, "settings/edit_server.html", data) + server = form.save() + return redirect("settings-federated-server", server.id) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required("bookwyrm.control_federation", raise_exception=True), + name="dispatch", +) +class ImportServerBlocklist(View): + """manually add a server""" + + def get(self, request): + """add server form""" + return TemplateResponse(request, "settings/server_blocklist.html") + + def post(self, request): + """add a server from the admin panel""" + json_data = json.load(request.FILES["json_file"]) + failed = [] + success_count = 0 + for item in json_data: + server_name = item.get("instance") + if not server_name: + failed.append(item) + continue + info_link = item.get("url") + + with transaction.atomic(): + server, _ = models.FederatedServer.objects.get_or_create( + server_name=server_name, + ) + server.notes = info_link + server.save() + server.block() + success_count += 1 + data = {"failed": failed, "succeeded": success_count} + return TemplateResponse(request, "settings/server_blocklist.html", data) + + @method_decorator(login_required, name="dispatch") @method_decorator( permission_required("bookwyrm.control_federation", raise_exception=True), name="dispatch", ) class FederatedServer(View): - """ views for handling a specific federated server """ + """views for handling a specific federated server""" def get(self, request, server): - """ load a server """ + """load a server""" server = get_object_or_404(models.FederatedServer, id=server) users = server.user_set data = { @@ -61,3 +119,32 @@ class FederatedServer(View): ), } return TemplateResponse(request, "settings/federated_server.html", data) + + def post(self, request, server): # pylint: disable=unused-argument + """update note""" + server = get_object_or_404(models.FederatedServer, id=server) + server.notes = request.POST.get("notes") + server.save() + return redirect("settings-federated-server", server.id) + + +@login_required +@require_POST +@permission_required("bookwyrm.control_federation", raise_exception=True) +# pylint: disable=unused-argument +def block_server(request, server): + """block a server""" + server = get_object_or_404(models.FederatedServer, id=server) + server.block() + return redirect("settings-federated-server", server.id) + + +@login_required +@require_POST +@permission_required("bookwyrm.control_federation", raise_exception=True) +# pylint: disable=unused-argument +def unblock_server(request, server): + """unblock a server""" + server = get_object_or_404(models.FederatedServer, id=server) + server.unblock() + return redirect("settings-federated-server", server.id) diff --git a/bookwyrm/views/feed.py b/bookwyrm/views/feed.py index e4be50e3..98f365ea 100644 --- a/bookwyrm/views/feed.py +++ b/bookwyrm/views/feed.py @@ -12,26 +12,20 @@ from bookwyrm import activitystreams, forms, models from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.settings import PAGE_LENGTH, STREAMS from .helpers import get_user_from_username, privacy_filter, get_suggested_users -from .helpers import is_api_request, is_bookwyrm_request, object_visible_to_user +from .helpers import is_api_request, is_bookwyrm_request # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class Feed(View): - """ activity stream """ + """activity stream""" def get(self, request, tab): - """ user's homepage with activity feed """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - + """user's homepage with activity feed""" if not tab in STREAMS: tab = "home" activities = activitystreams.streams[tab].get_activity_stream(request.user) - paginated = Paginator(activities, PAGE_LENGTH) suggested_users = get_suggested_users(request.user) @@ -40,7 +34,7 @@ class Feed(View): **feed_page_data(request.user), **{ "user": request.user, - "activities": paginated.page(page), + "activities": paginated.get_page(request.GET.get("page")), "suggested_users": suggested_users, "tab": tab, "goal_form": forms.GoalForm(), @@ -52,15 +46,10 @@ class Feed(View): @method_decorator(login_required, name="dispatch") class DirectMessage(View): - """ dm view """ + """dm view""" def get(self, request, username=None): - """ like a feed but for dms only """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - + """like a feed but for dms only""" # remove fancy subclasses of status, keep just good ol' notes queryset = models.Status.objects.filter( review__isnull=True, @@ -83,13 +72,12 @@ class DirectMessage(View): ).order_by("-published_date") paginated = Paginator(activities, PAGE_LENGTH) - activity_page = paginated.page(page) data = { **feed_page_data(request.user), **{ "user": request.user, "partner": user, - "activities": activity_page, + "activities": paginated.get_page(request.GET.get("page")), "path": "/direct-messages", }, } @@ -97,16 +85,16 @@ class DirectMessage(View): class Status(View): - """ get posting """ + """get posting""" def get(self, request, username, status_id): - """ display a particular status (and replies, etc) """ + """display a particular status (and replies, etc)""" try: user = get_user_from_username(request.user, username) status = models.Status.objects.select_subclasses().get( id=status_id, deleted=False ) - except (ValueError, models.Status.DoesNotExist): + except (ValueError, models.Status.DoesNotExist, models.User.DoesNotExist): return HttpResponseNotFound() # the url should have the poster's username in it @@ -114,7 +102,7 @@ class Status(View): return HttpResponseNotFound() # make sure the user is authorized to see the status - if not object_visible_to_user(request.user, status): + if not status.visible_to_user(request.user): return HttpResponseNotFound() if is_api_request(request): @@ -132,10 +120,10 @@ class Status(View): class Replies(View): - """ replies page (a json view of status) """ + """replies page (a json view of status)""" def get(self, request, username, status_id): - """ ordered collection of replies to a status """ + """ordered collection of replies to a status""" # the html view is the same as Status if not is_api_request(request): status_view = Status.as_view() @@ -150,7 +138,7 @@ class Replies(View): def feed_page_data(user): - """ info we need for every feed page """ + """info we need for every feed page""" if not user.is_authenticated: return {} @@ -163,7 +151,7 @@ def feed_page_data(user): def get_suggested_books(user, max_books=5): - """ helper to get a user's recent books """ + """helper to get a user's recent books""" book_count = 0 preset_shelves = [("reading", max_books), ("read", 2), ("to-read", max_books)] suggested_books = [] @@ -175,7 +163,7 @@ def get_suggested_books(user, max_books=5): ) shelf = user.shelf_set.get(identifier=preset) - shelf_books = shelf.shelfbook_set.order_by("-updated_date").all()[:limit] + shelf_books = shelf.shelfbook_set.order_by("-updated_date")[:limit] if not shelf_books: continue shelf_preview = { diff --git a/bookwyrm/views/follow.py b/bookwyrm/views/follow.py index d9f455eb..09c2d53a 100644 --- a/bookwyrm/views/follow.py +++ b/bookwyrm/views/follow.py @@ -12,7 +12,7 @@ from .helpers import get_user_from_username @login_required @require_POST def follow(request): - """ follow another user, here or abroad """ + """follow another user, here or abroad""" username = request.POST["user"] try: to_follow = get_user_from_username(request.user, username) @@ -33,7 +33,7 @@ def follow(request): @login_required @require_POST def unfollow(request): - """ unfollow a user """ + """unfollow a user""" username = request.POST["user"] try: to_unfollow = get_user_from_username(request.user, username) @@ -61,7 +61,7 @@ def unfollow(request): @login_required @require_POST def accept_follow_request(request): - """ a user accepts a follow request """ + """a user accepts a follow request""" username = request.POST["user"] try: requester = get_user_from_username(request.user, username) @@ -83,7 +83,7 @@ def accept_follow_request(request): @login_required @require_POST def delete_follow_request(request): - """ a user rejects a follow request """ + """a user rejects a follow request""" username = request.POST["user"] try: requester = get_user_from_username(request.user, username) diff --git a/bookwyrm/views/get_started.py b/bookwyrm/views/get_started.py index a21723a3..5573bf19 100644 --- a/bookwyrm/views/get_started.py +++ b/bookwyrm/views/get_started.py @@ -20,12 +20,12 @@ from .user import save_user_form # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class GetStartedProfile(View): - """ tell us about yourself """ + """tell us about yourself""" next_view = "get-started-books" def get(self, request): - """ basic profile info """ + """basic profile info""" data = { "form": forms.LimitedEditUserForm(instance=request.user), "next": self.next_view, @@ -33,7 +33,7 @@ class GetStartedProfile(View): return TemplateResponse(request, "get_started/profile.html", data) def post(self, request): - """ update your profile """ + """update your profile""" form = forms.LimitedEditUserForm( request.POST, request.FILES, instance=request.user ) @@ -46,12 +46,12 @@ class GetStartedProfile(View): @method_decorator(login_required, name="dispatch") class GetStartedBooks(View): - """ name a book, any book, we gotta start somewhere """ + """name a book, any book, we gotta start somewhere""" next_view = "get-started-users" def get(self, request): - """ info about a book """ + """info about a book""" query = request.GET.get("query") book_results = popular_books = [] if query: @@ -82,7 +82,7 @@ class GetStartedBooks(View): return TemplateResponse(request, "get_started/books.html", data) def post(self, request): - """ shelve some books """ + """shelve some books""" shelve_actions = [ (k, v) for k, v in request.POST.items() @@ -100,10 +100,10 @@ class GetStartedBooks(View): @method_decorator(login_required, name="dispatch") class GetStartedUsers(View): - """ find friends """ + """find friends""" def get(self, request): - """ basic profile info """ + """basic profile info""" query = request.GET.get("query") user_results = ( models.User.viewer_aware_objects(request.user) diff --git a/bookwyrm/views/goal.py b/bookwyrm/views/goal.py index 9c4e117c..84091fe3 100644 --- a/bookwyrm/views/goal.py +++ b/bookwyrm/views/goal.py @@ -10,23 +10,23 @@ from django.views.decorators.http import require_POST from bookwyrm import forms, models from bookwyrm.status import create_generated_note -from .helpers import get_user_from_username, object_visible_to_user +from .helpers import get_user_from_username # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class Goal(View): - """ track books for the year """ + """track books for the year""" def get(self, request, username, year): - """ reading goal page """ + """reading goal page""" user = get_user_from_username(request.user, username) year = int(year) goal = models.AnnualGoal.objects.filter(year=year, user=user).first() if not goal and user != request.user: return HttpResponseNotFound() - if goal and not object_visible_to_user(request.user, goal): + if goal and not goal.visible_to_user(request.user): return HttpResponseNotFound() data = { @@ -39,7 +39,7 @@ class Goal(View): return TemplateResponse(request, "goal.html", data) def post(self, request, username, year): - """ update or create an annual goal """ + """update or create an annual goal""" user = get_user_from_username(request.user, username) if user != request.user: return HttpResponseNotFound() @@ -71,7 +71,7 @@ class Goal(View): @require_POST @login_required def hide_goal(request): - """ don't keep bugging people to set a goal """ + """don't keep bugging people to set a goal""" request.user.show_goal = False request.user.save(broadcast=False) return redirect(request.headers.get("Referer", "/")) diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py index 75c5da8f..8a60b54c 100644 --- a/bookwyrm/views/helpers.py +++ b/bookwyrm/views/helpers.py @@ -11,7 +11,7 @@ from bookwyrm.utils import regex def get_user_from_username(viewer, username): - """ helper function to resolve a localname or a username to a user """ + """helper function to resolve a localname or a username to a user""" # raises DoesNotExist if user is now found try: return models.User.viewer_aware_objects(viewer).get(localname=username) @@ -20,44 +20,20 @@ def get_user_from_username(viewer, username): def is_api_request(request): - """ check whether a request is asking for html or data """ + """check whether a request is asking for html or data""" return "json" in request.headers.get("Accept", "") or request.path[-5:] == ".json" def is_bookwyrm_request(request): - """ check if the request is coming from another bookwyrm instance """ + """check if the request is coming from another bookwyrm instance""" user_agent = request.headers.get("User-Agent") if user_agent is None or re.search(regex.bookwyrm_user_agent, user_agent) is None: return False return True -def object_visible_to_user(viewer, obj): - """ is a user authorized to view an object? """ - if not obj: - return False - - # viewer can't see it if the object's owner blocked them - if viewer in obj.user.blocks.all(): - return False - - # you can see your own posts and any public or unlisted posts - if viewer == obj.user or obj.privacy in ["public", "unlisted"]: - return True - - # you can see the followers only posts of people you follow - if obj.privacy == "followers" and obj.user.followers.filter(id=viewer.id).first(): - return True - - # you can see dms you are tagged in - if isinstance(obj, models.Status): - if obj.privacy == "direct" and obj.mention_users.filter(id=viewer.id).first(): - return True - return False - - def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False): - """ filter objects that have "user" and "privacy" fields """ + """filter objects that have "user" and "privacy" fields""" privacy_levels = privacy_levels or ["public", "unlisted", "followers", "direct"] # if there'd a deleted field, exclude deleted items try: @@ -108,7 +84,7 @@ def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False): def handle_remote_webfinger(query): - """ webfingerin' other servers """ + """webfingerin' other servers""" user = None # usernames could be @user@domain or user@domain @@ -124,7 +100,7 @@ def handle_remote_webfinger(query): return None try: - user = models.User.objects.get(username=query) + user = models.User.objects.get(username__iexact=query) except models.User.DoesNotExist: url = "https://%s/.well-known/webfinger?resource=acct:%s" % (domain, query) try: @@ -138,13 +114,13 @@ def handle_remote_webfinger(query): user = activitypub.resolve_remote_id( link["href"], model=models.User ) - except KeyError: + except (KeyError, activitypub.ActivitySerializerError): return None return user def get_edition(book_id): - """ look up a book in the db and return an edition """ + """look up a book in the db and return an edition""" book = models.Book.objects.select_subclasses().get(id=book_id) if isinstance(book, models.Work): book = book.get_default_edition() @@ -152,7 +128,7 @@ def get_edition(book_id): def handle_reading_status(user, shelf, book, privacy): - """ post about a user reading a book """ + """post about a user reading a book""" # tell the world about this cool thing that happened try: message = { @@ -169,14 +145,14 @@ def handle_reading_status(user, shelf, book, privacy): def is_blocked(viewer, user): - """ is this viewer blocked by the user? """ + """is this viewer blocked by the user?""" if viewer.is_authenticated and viewer in user.blocks.all(): return True return False def get_discover_books(): - """ list of books for the discover page """ + """list of books for the discover page""" return list( set( models.Edition.objects.filter( @@ -193,7 +169,7 @@ def get_discover_books(): def get_suggested_users(user): - """ bookwyrm users you don't already know """ + """bookwyrm users you don't already know""" return ( get_annotated_users( user, @@ -208,7 +184,7 @@ def get_suggested_users(user): def get_annotated_users(user, *args, **kwargs): - """ Users, annotated with things they have in common """ + """Users, annotated with things they have in common""" return ( models.User.objects.filter(discoverable=True, is_active=True, *args, **kwargs) .exclude(Q(id__in=user.blocks.all()) | Q(blocks=user)) diff --git a/bookwyrm/views/import_data.py b/bookwyrm/views/import_data.py index 5bdbe915..a2abbc69 100644 --- a/bookwyrm/views/import_data.py +++ b/bookwyrm/views/import_data.py @@ -16,10 +16,10 @@ from bookwyrm.tasks import app # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class Import(View): - """ import view """ + """import view""" def get(self, request): - """ load import page """ + """load import page""" return TemplateResponse( request, "import.html", @@ -32,7 +32,7 @@ class Import(View): ) def post(self, request): - """ ingest a goodreads csv """ + """ingest a goodreads csv""" form = forms.ImportForm(request.POST, request.FILES) if form.is_valid(): include_reviews = request.POST.get("include_reviews") == "on" @@ -66,10 +66,10 @@ class Import(View): @method_decorator(login_required, name="dispatch") class ImportStatus(View): - """ status of an existing import """ + """status of an existing import""" def get(self, request, job_id): - """ status of an import job """ + """status of an import job""" job = models.ImportJob.objects.get(id=job_id) if job.user != request.user: raise PermissionDenied @@ -84,7 +84,7 @@ class ImportStatus(View): ) def post(self, request, job_id): - """ retry lines from an import """ + """retry lines from an import""" job = get_object_or_404(models.ImportJob, id=job_id) items = [] for item in request.POST.getlist("import_item"): diff --git a/bookwyrm/views/inbox.py b/bookwyrm/views/inbox.py index 34bd2e1c..a558c571 100644 --- a/bookwyrm/views/inbox.py +++ b/bookwyrm/views/inbox.py @@ -1,9 +1,10 @@ """ incoming activities """ import json +import re from urllib.parse import urldefrag -from django.http import HttpResponse -from django.http import HttpResponseBadRequest, HttpResponseNotFound +from django.http import HttpResponse, HttpResponseNotFound +from django.http import HttpResponseBadRequest, HttpResponseForbidden from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.csrf import csrf_exempt @@ -12,15 +13,20 @@ import requests from bookwyrm import activitypub, models from bookwyrm.tasks import app from bookwyrm.signatures import Signature +from bookwyrm.utils import regex @method_decorator(csrf_exempt, name="dispatch") # pylint: disable=no-self-use class Inbox(View): - """ requests sent by outside servers""" + """requests sent by outside servers""" def post(self, request, username=None): - """ only works as POST request """ + """only works as POST request""" + # first check if this server is on our shitlist + if is_blocked_user_agent(request): + return HttpResponseForbidden() + # make sure the user's inbox even exists if username: try: @@ -34,6 +40,10 @@ class Inbox(View): except json.decoder.JSONDecodeError: return HttpResponseBadRequest() + # let's be extra sure we didn't block this domain + if is_blocked_activity(activity_json): + return HttpResponseForbidden() + if ( not "object" in activity_json or not "type" in activity_json @@ -54,26 +64,47 @@ class Inbox(View): return HttpResponse() +def is_blocked_user_agent(request): + """check if a request is from a blocked server based on user agent""" + # check user agent + user_agent = request.headers.get("User-Agent") + if not user_agent: + return False + url = re.search(r"https?://{:s}/?".format(regex.domain), user_agent) + if not url: + return False + url = url.group() + return models.FederatedServer.is_blocked(url) + + +def is_blocked_activity(activity_json): + """get the sender out of activity json and check if it's blocked""" + actor = activity_json.get("actor") + + # check if the user is banned/deleted + existing = models.User.find_existing_by_remote_id(actor) + if existing and existing.deleted: + return True + + if not actor: + # well I guess it's not even a valid activity so who knows + return False + return models.FederatedServer.is_blocked(actor) + + @app.task def activity_task(activity_json): - """ do something with this json we think is legit """ + """do something with this json we think is legit""" # lets see if the activitypub module can make sense of this json - try: - activity = activitypub.parse(activity_json) - except activitypub.ActivitySerializerError: - return + activity = activitypub.parse(activity_json) # cool that worked, now we should do the action described by the type # (create, update, delete, etc) - try: - activity.action() - except activitypub.ActivitySerializerError: - # this is raised if the activity is discarded - return + activity.action() def has_valid_signature(request, activity): - """ verify incoming signature """ + """verify incoming signature""" try: signature = Signature.parse(request) diff --git a/bookwyrm/views/interaction.py b/bookwyrm/views/interaction.py index e337f2ef..e138e41c 100644 --- a/bookwyrm/views/interaction.py +++ b/bookwyrm/views/interaction.py @@ -12,10 +12,10 @@ from bookwyrm import models # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class Favorite(View): - """ like a status """ + """like a status""" def post(self, request, status_id): - """ create a like """ + """create a like""" status = models.Status.objects.get(id=status_id) try: models.Favorite.objects.create(status=status, user=request.user) @@ -28,10 +28,10 @@ class Favorite(View): @method_decorator(login_required, name="dispatch") class Unfavorite(View): - """ take back a fav """ + """take back a fav""" def post(self, request, status_id): - """ unlike a status """ + """unlike a status""" status = models.Status.objects.get(id=status_id) try: favorite = models.Favorite.objects.get(status=status, user=request.user) @@ -45,10 +45,10 @@ class Unfavorite(View): @method_decorator(login_required, name="dispatch") class Boost(View): - """ boost a status """ + """boost a status""" def post(self, request, status_id): - """ boost a status """ + """boost a status""" status = models.Status.objects.get(id=status_id) # is it boostable? if not status.boostable: @@ -70,10 +70,10 @@ class Boost(View): @method_decorator(login_required, name="dispatch") class Unboost(View): - """ boost a status """ + """boost a status""" def post(self, request, status_id): - """ boost a status """ + """boost a status""" status = models.Status.objects.get(id=status_id) boost = models.Boost.objects.filter( boosted_status=status, user=request.user diff --git a/bookwyrm/views/invite.py b/bookwyrm/views/invite.py index 61f031ef..92f930f4 100644 --- a/bookwyrm/views/invite.py +++ b/bookwyrm/views/invite.py @@ -26,15 +26,10 @@ from . import helpers name="dispatch", ) class ManageInvites(View): - """ create invites """ + """create invites""" def get(self, request): - """ invite management page """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - + """invite management page""" paginated = Paginator( models.SiteInvite.objects.filter(user=request.user).order_by( "-created_date" @@ -43,13 +38,13 @@ class ManageInvites(View): ) data = { - "invites": paginated.page(page), + "invites": paginated.get_page(request.GET.get("page")), "form": forms.CreateInviteForm(), } return TemplateResponse(request, "settings/manage_invites.html", data) def post(self, request): - """ creates an invite database entry """ + """creates an invite database entry""" form = forms.CreateInviteForm(request.POST) if not form.is_valid(): return HttpResponseBadRequest("ERRORS : %s" % (form.errors,)) @@ -69,10 +64,10 @@ class ManageInvites(View): class Invite(View): - """ use an invite to register """ + """use an invite to register""" def get(self, request, code): - """ endpoint for using an invites """ + """endpoint for using an invites""" if request.user.is_authenticated: return redirect("/") invite = get_object_or_404(models.SiteInvite, code=code) @@ -88,16 +83,11 @@ class Invite(View): class ManageInviteRequests(View): - """ grant invites like the benevolent lord you are """ + """grant invites like the benevolent lord you are""" def get(self, request): - """ view a list of requests """ + """view a list of requests""" ignored = request.GET.get("ignored", False) - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - sort = request.GET.get("sort") sort_fields = [ "created_date", @@ -136,13 +126,13 @@ class ManageInviteRequests(View): data = { "ignored": ignored, "count": paginated.count, - "requests": paginated.page(page), + "requests": paginated.get_page(request.GET.get("page")), "sort": sort, } return TemplateResponse(request, "settings/manage_invite_requests.html", data) def post(self, request): - """ send out an invite """ + """send out an invite""" invite_request = get_object_or_404( models.InviteRequest, id=request.POST.get("invite-request") ) @@ -162,10 +152,10 @@ class ManageInviteRequests(View): class InviteRequest(View): - """ prospective users sign up here """ + """prospective users sign up here""" def post(self, request): - """ create a request """ + """create a request""" form = forms.InviteRequestForm(request.POST) received = False if form.is_valid(): @@ -182,7 +172,7 @@ class InviteRequest(View): @require_POST def ignore_invite_request(request): - """ hide an invite request """ + """hide an invite request""" invite_request = get_object_or_404( models.InviteRequest, id=request.POST.get("invite-request") ) diff --git a/bookwyrm/views/isbn.py b/bookwyrm/views/isbn.py index b7ba02dd..197088ba 100644 --- a/bookwyrm/views/isbn.py +++ b/bookwyrm/views/isbn.py @@ -13,10 +13,10 @@ from .helpers import is_api_request # pylint: disable= no-self-use class Isbn(View): - """ search a book by isbn """ + """search a book by isbn""" def get(self, request, isbn): - """ info about a book """ + """info about a book""" book_results = connector_manager.isbn_local_search(isbn) if is_api_request(request): diff --git a/bookwyrm/views/landing.py b/bookwyrm/views/landing.py index 407451fb..1361935e 100644 --- a/bookwyrm/views/landing.py +++ b/bookwyrm/views/landing.py @@ -9,18 +9,18 @@ from . import helpers # pylint: disable= no-self-use class About(View): - """ create invites """ + """create invites""" def get(self, request): - """ more information about the instance """ + """more information about the instance""" return TemplateResponse(request, "discover/about.html") class Home(View): - """ discover page or home feed depending on auth """ + """discover page or home feed depending on auth""" def get(self, request): - """ this is the same as the feed on the home tab """ + """this is the same as the feed on the home tab""" if request.user.is_authenticated: feed_view = Feed.as_view() return feed_view(request, "home") @@ -29,10 +29,10 @@ class Home(View): class Discover(View): - """ preview of recently reviewed books """ + """preview of recently reviewed books""" def get(self, request): - """ tiled book activity page """ + """tiled book activity page""" data = { "register_form": forms.RegisterForm(), "request_form": forms.InviteRequestForm(), diff --git a/bookwyrm/views/list.py b/bookwyrm/views/list.py index 7724cd13..89b936e9 100644 --- a/bookwyrm/views/list.py +++ b/bookwyrm/views/list.py @@ -1,11 +1,16 @@ """ book list views""" +from typing import Optional +from urllib.parse import urlencode + from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator -from django.db import IntegrityError -from django.db.models import Count, Q -from django.http import HttpResponseNotFound, HttpResponseBadRequest +from django.db import IntegrityError, transaction +from django.db.models import Avg, Count, Q, Max +from django.db.models.functions import Coalesce +from django.http import HttpResponseNotFound, HttpResponseBadRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse +from django.urls import reverse from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.http import require_POST @@ -13,20 +18,16 @@ from django.views.decorators.http import require_POST from bookwyrm import forms, models from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.connectors import connector_manager -from .helpers import is_api_request, object_visible_to_user, privacy_filter +from .helpers import is_api_request, privacy_filter from .helpers import get_user_from_username + # pylint: disable=no-self-use class Lists(View): - """ book list page """ + """book list page""" def get(self, request): - """ display a book list """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - + """display a book list""" # hide lists with no approved books lists = ( models.List.objects.annotate( @@ -35,7 +36,6 @@ class Lists(View): .filter(item_count__gt=0) .order_by("-updated_date") .distinct() - .all() ) lists = privacy_filter( @@ -44,7 +44,7 @@ class Lists(View): paginated = Paginator(lists, 12) data = { - "lists": paginated.page(page), + "lists": paginated.get_page(request.GET.get("page")), "list_form": forms.ListForm(), "path": "/list", } @@ -53,7 +53,7 @@ class Lists(View): @method_decorator(login_required, name="dispatch") # pylint: disable=unused-argument def post(self, request): - """ create a book_list """ + """create a book_list""" form = forms.ListForm(request.POST) if not form.is_valid(): return redirect("lists") @@ -63,23 +63,19 @@ class Lists(View): class UserLists(View): - """ a user's book list page """ + """a user's book list page""" def get(self, request, username): - """ display a book list """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 + """display a book list""" user = get_user_from_username(request.user, username) - lists = models.List.objects.filter(user=user).all() + lists = models.List.objects.filter(user=user) lists = privacy_filter(request.user, lists) paginated = Paginator(lists, 12) data = { "user": user, "is_self": request.user.id == user.id, - "lists": paginated.page(page), + "lists": paginated.get_page(request.GET.get("page")), "list_form": forms.ListForm(), "path": user.local_path + "/lists", } @@ -87,12 +83,12 @@ class UserLists(View): class List(View): - """ book list page """ + """book list page""" def get(self, request, list_id): - """ display a book list """ + """display a book list""" book_list = get_object_or_404(models.List, id=list_id) - if not object_visible_to_user(request.user, book_list): + if not book_list.visible_to_user(request.user): return HttpResponseNotFound() if is_api_request(request): @@ -100,9 +96,52 @@ class List(View): query = request.GET.get("q") suggestions = None + + # sort_by shall be "order" unless a valid alternative is given + sort_by = request.GET.get("sort_by", "order") + if sort_by not in ("order", "title", "rating"): + sort_by = "order" + + # direction shall be "ascending" unless a valid alternative is given + direction = request.GET.get("direction", "ascending") + if direction not in ("ascending", "descending"): + direction = "ascending" + + internal_sort_by = { + "order": "order", + "title": "book__title", + "rating": "average_rating", + } + directional_sort_by = internal_sort_by[sort_by] + if direction == "descending": + directional_sort_by = "-" + directional_sort_by + + if sort_by == "order": + items = book_list.listitem_set.filter(approved=True).order_by( + directional_sort_by + ) + elif sort_by == "title": + items = book_list.listitem_set.filter(approved=True).order_by( + directional_sort_by + ) + elif sort_by == "rating": + items = ( + book_list.listitem_set.annotate( + average_rating=Avg(Coalesce("book__review__rating", 0)) + ) + .filter(approved=True) + .order_by(directional_sort_by) + ) + + paginated = Paginator(items, 12) + if query and request.user.is_authenticated: # search for books - suggestions = connector_manager.local_search(query, raw=True) + suggestions = connector_manager.local_search( + query, + raw=True, + filters=[~Q(parent_work__editions__in=book_list.books.all())], + ) elif request.user.is_authenticated: # just suggest whatever books are nearby suggestions = request.user.shelfbook_set.filter( @@ -119,18 +158,21 @@ class List(View): data = { "list": book_list, - "items": book_list.listitem_set.filter(approved=True), + "items": paginated.get_page(request.GET.get("page")), "pending_count": book_list.listitem_set.filter(approved=False).count(), "suggested_books": suggestions, "list_form": forms.ListForm(instance=book_list), "query": query or "", + "sort_form": forms.SortListForm( + {"direction": direction, "sort_by": sort_by} + ), } return TemplateResponse(request, "lists/list.html", data) @method_decorator(login_required, name="dispatch") # pylint: disable=unused-argument def post(self, request, list_id): - """ edit a list """ + """edit a list""" book_list = get_object_or_404(models.List, id=list_id) form = forms.ListForm(request.POST, instance=book_list) if not form.is_valid(): @@ -140,11 +182,11 @@ class List(View): class Curate(View): - """ approve or discard list suggestsions """ + """approve or discard list suggestsions""" @method_decorator(login_required, name="dispatch") def get(self, request, list_id): - """ display a pending list """ + """display a pending list""" book_list = get_object_or_404(models.List, id=list_id) if not book_list.user == request.user: # only the creater can curate the list @@ -160,42 +202,65 @@ class Curate(View): @method_decorator(login_required, name="dispatch") # pylint: disable=unused-argument def post(self, request, list_id): - """ edit a book_list """ + """edit a book_list""" book_list = get_object_or_404(models.List, id=list_id) suggestion = get_object_or_404(models.ListItem, id=request.POST.get("item")) approved = request.POST.get("approved") == "true" if approved: + # update the book and set it to be the last in the order of approved books, + # before any pending books suggestion.approved = True + order_max = ( + book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[ + "order__max" + ] + or 0 + ) + 1 + suggestion.order = order_max + increment_order_in_reverse(book_list.id, order_max) suggestion.save() else: - suggestion.delete() + deleted_order = suggestion.order + suggestion.delete(broadcast=False) + normalize_book_list_ordering(book_list.id, start=deleted_order) return redirect("list-curate", book_list.id) @require_POST def add_book(request): - """ put a book on a list """ + """put a book on a list""" book_list = get_object_or_404(models.List, id=request.POST.get("list")) - if not object_visible_to_user(request.user, book_list): + if not book_list.visible_to_user(request.user): return HttpResponseNotFound() book = get_object_or_404(models.Edition, id=request.POST.get("book")) # do you have permission to add to the list? try: if request.user == book_list.user or book_list.curation == "open": - # go ahead and add it + # add the book at the latest order of approved books, before pending books + order_max = ( + book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[ + "order__max" + ] + ) or 0 + increment_order_in_reverse(book_list.id, order_max + 1) models.ListItem.objects.create( book=book, book_list=book_list, user=request.user, + order=order_max + 1, ) elif book_list.curation == "curated": - # make a pending entry + # make a pending entry at the end of the list + order_max = ( + book_list.listitem_set.aggregate(Max("order"))["order__max"] + ) or 0 models.ListItem.objects.create( approved=False, book=book, book_list=book_list, user=request.user, + order=order_max + 1, ) else: # you can't add to this list, what were you THINKING @@ -204,17 +269,121 @@ def add_book(request): # if the book is already on the list, don't flip out pass - return redirect("list", book_list.id) + path = reverse("list", args=[book_list.id]) + params = request.GET.copy() + params["updated"] = True + return redirect("{:s}?{:s}".format(path, urlencode(params))) @require_POST def remove_book(request, list_id): - """ put a book on a list """ - book_list = get_object_or_404(models.List, id=list_id) - item = get_object_or_404(models.ListItem, id=request.POST.get("item")) + """remove a book from a list""" + with transaction.atomic(): + book_list = get_object_or_404(models.List, id=list_id) + item = get_object_or_404(models.ListItem, id=request.POST.get("item")) - if not book_list.user == request.user and not item.user == request.user: - return HttpResponseNotFound() + if not book_list.user == request.user and not item.user == request.user: + return HttpResponseNotFound() - item.delete() + deleted_order = item.order + item.delete() + normalize_book_list_ordering(book_list.id, start=deleted_order) return redirect("list", list_id) + + +@require_POST +def set_book_position(request, list_item_id): + """ + Action for when the list user manually specifies a list position, takes + special care with the unique ordering per list. + """ + with transaction.atomic(): + list_item = get_object_or_404(models.ListItem, id=list_item_id) + try: + int_position = int(request.POST.get("position")) + except ValueError: + return HttpResponseBadRequest( + "bad value for position. should be an integer" + ) + + if int_position < 1: + return HttpResponseBadRequest("position cannot be less than 1") + + book_list = list_item.book_list + + # the max position to which a book may be set is the highest order for + # books which are approved + order_max = book_list.listitem_set.filter(approved=True).aggregate( + Max("order") + )["order__max"] + + if int_position > order_max: + int_position = order_max + + if request.user not in (book_list.user, list_item.user): + return HttpResponseNotFound() + + original_order = list_item.order + if original_order == int_position: + return HttpResponse(status=204) + if original_order > int_position: + list_item.order = -1 + list_item.save() + increment_order_in_reverse(book_list.id, int_position, original_order) + else: + list_item.order = -1 + list_item.save() + decrement_order(book_list.id, original_order, int_position) + + list_item.order = int_position + list_item.save() + + return redirect("list", book_list.id) + + +@transaction.atomic +def increment_order_in_reverse( + book_list_id: int, start: int, end: Optional[int] = None +): + """increase the order nu,ber for every item in a list""" + try: + book_list = models.List.objects.get(id=book_list_id) + except models.List.DoesNotExist: + return + items = book_list.listitem_set.filter(order__gte=start) + if end is not None: + items = items.filter(order__lt=end) + items = items.order_by("-order") + for item in items: + item.order += 1 + item.save() + + +@transaction.atomic +def decrement_order(book_list_id, start, end): + """decrement the order value for every item in a list""" + try: + book_list = models.List.objects.get(id=book_list_id) + except models.List.DoesNotExist: + return + items = book_list.listitem_set.filter(order__gt=start, order__lte=end).order_by( + "order" + ) + for item in items: + item.order -= 1 + item.save() + + +@transaction.atomic +def normalize_book_list_ordering(book_list_id, start=0, add_offset=0): + """gives each book in a list the proper sequential order number""" + try: + book_list = models.List.objects.get(id=book_list_id) + except models.List.DoesNotExist: + return + items = book_list.listitem_set.filter(order__gt=start).order_by("order") + for i, item in enumerate(items, start): + effective_order = i + add_offset + if item.order != effective_order: + item.order = effective_order + item.save() diff --git a/bookwyrm/views/notifications.py b/bookwyrm/views/notifications.py index 7a62ec01..3d08cade 100644 --- a/bookwyrm/views/notifications.py +++ b/bookwyrm/views/notifications.py @@ -9,20 +9,20 @@ from django.views import View # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class Notifications(View): - """ notifications view """ + """notifications view""" def get(self, request): - """ people are interacting with you, get hyped """ + """people are interacting with you, get hyped""" notifications = request.user.notification_set.all().order_by("-created_date") unread = [n.id for n in notifications.filter(read=False)] data = { - "notifications": notifications, + "notifications": notifications[:50], "unread": unread, } notifications.update(read=True) return TemplateResponse(request, "notifications.html", data) def post(self, request): - """ permanently delete notification for user """ + """permanently delete notification for user""" request.user.notification_set.filter(read=True).delete() return redirect("/notifications") diff --git a/bookwyrm/views/outbox.py b/bookwyrm/views/outbox.py index ec6f5cd3..4bc2d2b9 100644 --- a/bookwyrm/views/outbox.py +++ b/bookwyrm/views/outbox.py @@ -9,10 +9,10 @@ from .helpers import is_bookwyrm_request # pylint: disable= no-self-use class Outbox(View): - """ outbox """ + """outbox""" def get(self, request, username): - """ outbox for the requested user """ + """outbox for the requested user""" user = get_object_or_404(models.User, localname=username) filter_type = request.GET.get("type") if filter_type not in models.status_models: diff --git a/bookwyrm/views/password.py b/bookwyrm/views/password.py index 67010974..933817a8 100644 --- a/bookwyrm/views/password.py +++ b/bookwyrm/views/password.py @@ -14,17 +14,17 @@ from bookwyrm.emailing import password_reset_email # pylint: disable= no-self-use class PasswordResetRequest(View): - """ forgot password flow """ + """forgot password flow""" def get(self, request): - """ password reset page """ + """password reset page""" return TemplateResponse( request, "password_reset_request.html", ) def post(self, request): - """ create a password reset token """ + """create a password reset token""" email = request.POST.get("email") try: user = models.User.objects.get(email=email) @@ -43,10 +43,10 @@ class PasswordResetRequest(View): class PasswordReset(View): - """ set new password """ + """set new password""" def get(self, request, code): - """ endpoint for sending invites """ + """endpoint for sending invites""" if request.user.is_authenticated: return redirect("/") try: @@ -59,7 +59,7 @@ class PasswordReset(View): return TemplateResponse(request, "password_reset.html", {"code": code}) def post(self, request, code): - """ allow a user to change their password through an emailed token """ + """allow a user to change their password through an emailed token""" try: reset_code = models.PasswordReset.objects.get(code=code) except models.PasswordReset.DoesNotExist: @@ -84,15 +84,15 @@ class PasswordReset(View): @method_decorator(login_required, name="dispatch") class ChangePassword(View): - """ change password as logged in user """ + """change password as logged in user""" def get(self, request): - """ change password page """ + """change password page""" data = {"user": request.user} return TemplateResponse(request, "preferences/change_password.html", data) def post(self, request): - """ allow a user to change their password """ + """allow a user to change their password""" new_password = request.POST.get("password") confirm_password = request.POST.get("confirm-password") diff --git a/bookwyrm/views/reading.py b/bookwyrm/views/reading.py index b780dd2f..65ca717d 100644 --- a/bookwyrm/views/reading.py +++ b/bookwyrm/views/reading.py @@ -18,7 +18,7 @@ from .shelf import handle_unshelve @login_required @require_POST def start_reading(request, book_id): - """ begin reading a book """ + """begin reading a book""" book = get_edition(book_id) reading_shelf = models.Shelf.objects.filter( identifier=models.Shelf.READING, user=request.user @@ -60,7 +60,7 @@ def start_reading(request, book_id): @login_required @require_POST def finish_reading(request, book_id): - """ a user completed a book, yay """ + """a user completed a book, yay""" book = get_edition(book_id) finished_read_shelf = models.Shelf.objects.filter( identifier=models.Shelf.READ_FINISHED, user=request.user @@ -101,7 +101,7 @@ def finish_reading(request, book_id): @login_required @require_POST def edit_readthrough(request): - """ can't use the form because the dates are too finnicky """ + """can't use the form because the dates are too finnicky""" readthrough = update_readthrough(request, create=False) if not readthrough: return HttpResponseNotFound() @@ -121,7 +121,7 @@ def edit_readthrough(request): @login_required @require_POST def delete_readthrough(request): - """ remove a readthrough """ + """remove a readthrough""" readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id")) # don't let people edit other people's data @@ -135,7 +135,7 @@ def delete_readthrough(request): @login_required @require_POST def create_readthrough(request): - """ can't use the form because the dates are too finnicky """ + """can't use the form because the dates are too finnicky""" book = get_object_or_404(models.Edition, id=request.POST.get("book")) readthrough = update_readthrough(request, create=True, book=book) if not readthrough: @@ -145,13 +145,14 @@ def create_readthrough(request): def load_date_in_user_tz_as_utc(date_str: str, user: models.User) -> datetime: + """ensures that data is stored consistently in the UTC timezone""" user_tz = dateutil.tz.gettz(user.preferred_timezone) start_date = dateutil.parser.parse(date_str, ignoretz=True) return start_date.replace(tzinfo=user_tz).astimezone(dateutil.tz.UTC) def update_readthrough(request, book=None, create=True): - """ updates but does not save dates on a readthrough """ + """updates but does not save dates on a readthrough""" try: read_id = request.POST.get("id") if not read_id: @@ -208,7 +209,7 @@ def update_readthrough(request, book=None, create=True): @login_required @require_POST def delete_progressupdate(request): - """ remove a progress update """ + """remove a progress update""" update = get_object_or_404(models.ProgressUpdate, id=request.POST.get("id")) # don't let people edit other people's data diff --git a/bookwyrm/views/reports.py b/bookwyrm/views/reports.py index cb1a62ff..46c23884 100644 --- a/bookwyrm/views/reports.py +++ b/bookwyrm/views/reports.py @@ -20,17 +20,19 @@ from bookwyrm import forms, models name="dispatch", ) class Reports(View): - """ list of reports """ + """list of reports""" def get(self, request): - """ view current reports """ + """view current reports""" filters = {} resolved = request.GET.get("resolved") == "true" server = request.GET.get("server") if server: - server = get_object_or_404(models.FederatedServer, id=server) - filters["user__federated_server"] = server + filters["user__federated_server__server_name"] = server + username = request.GET.get("username") + if username: + filters["user__username__icontains"] = username filters["resolved"] = resolved data = { "resolved": resolved, @@ -50,17 +52,17 @@ class Reports(View): name="dispatch", ) class Report(View): - """ view a specific report """ + """view a specific report""" def get(self, request, report_id): - """ load a report """ + """load a report""" data = { "report": get_object_or_404(models.Report, id=report_id), } return TemplateResponse(request, "moderation/report.html", data) def post(self, request, report_id): - """ comment on a report """ + """comment on a report""" report = get_object_or_404(models.Report, id=report_id) models.ReportComment.objects.create( user=request.user, @@ -72,18 +74,19 @@ class Report(View): @login_required @permission_required("bookwyrm_moderate_user") -def deactivate_user(_, report_id): - """ mark an account as inactive """ - report = get_object_or_404(models.Report, id=report_id) - report.user.is_active = not report.user.is_active - report.user.save() - return redirect("settings-report", report.id) +def suspend_user(_, user_id): + """mark an account as inactive""" + user = get_object_or_404(models.User, id=user_id) + user.is_active = not user.is_active + # this isn't a full deletion, so we don't want to tell the world + user.save(broadcast=False) + return redirect("settings-user", user.id) @login_required @permission_required("bookwyrm_moderate_post") def resolve_report(_, report_id): - """ mark a report as (un)resolved """ + """mark a report as (un)resolved""" report = get_object_or_404(models.Report, id=report_id) report.resolved = not report.resolved report.save() @@ -95,11 +98,10 @@ def resolve_report(_, report_id): @login_required @require_POST def make_report(request): - """ a user reports something """ + """a user reports something""" form = forms.ReportForm(request.POST) if not form.is_valid(): - print(form.errors) - return redirect(request.headers.get("Referer", "/")) + raise ValueError(form.errors) form.save() return redirect(request.headers.get("Referer", "/")) diff --git a/bookwyrm/views/rss_feed.py b/bookwyrm/views/rss_feed.py index ed3e84f4..f1678b7f 100644 --- a/bookwyrm/views/rss_feed.py +++ b/bookwyrm/views/rss_feed.py @@ -5,25 +5,25 @@ from .helpers import get_user_from_username, privacy_filter # pylint: disable=no-self-use, unused-argument class RssFeed(Feed): - """ serialize user's posts in rss feed """ + """serialize user's posts in rss feed""" description_template = "snippets/rss_content.html" title_template = "snippets/rss_title.html" def get_object(self, request, username): - """ the user who's posts get serialized """ + """the user who's posts get serialized""" return get_user_from_username(request.user, username) def link(self, obj): - """ link to the user's profile """ + """link to the user's profile""" return obj.local_path def title(self, obj): - """ title of the rss feed entry """ + """title of the rss feed entry""" return f"Status updates from {obj.display_name}" def items(self, obj): - """ the user's activity feed """ + """the user's activity feed""" return privacy_filter( obj, obj.status_set.select_subclasses(), @@ -31,5 +31,5 @@ class RssFeed(Feed): ) def item_link(self, item): - """ link to the status """ + """link to the status""" return item.local_path diff --git a/bookwyrm/views/search.py b/bookwyrm/views/search.py index 28f393c8..4543b55e 100644 --- a/bookwyrm/views/search.py +++ b/bookwyrm/views/search.py @@ -16,10 +16,10 @@ from .helpers import handle_remote_webfinger # pylint: disable= no-self-use class Search(View): - """ search users or books """ + """search users or books""" def get(self, request): - """ that search bar up top """ + """that search bar up top""" query = request.GET.get("q") min_confidence = request.GET.get("min_confidence", 0.1) @@ -34,7 +34,7 @@ class Search(View): if query and re.match(regex.full_username, query): handle_remote_webfinger(query) - # do a user search + # do a user search user_results = ( models.User.viewer_aware_objects(request.user) .annotate( diff --git a/bookwyrm/views/shelf.py b/bookwyrm/views/shelf.py index 41d1f135..9bcf0a4a 100644 --- a/bookwyrm/views/shelf.py +++ b/bookwyrm/views/shelf.py @@ -16,25 +16,20 @@ from bookwyrm import forms, models from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.settings import PAGE_LENGTH from .helpers import is_api_request, get_edition, get_user_from_username -from .helpers import handle_reading_status, privacy_filter, object_visible_to_user +from .helpers import handle_reading_status, privacy_filter # pylint: disable= no-self-use class Shelf(View): - """ shelf page """ + """shelf page""" def get(self, request, username, shelf_identifier=None): - """ display a shelf """ + """display a shelf""" try: user = get_user_from_username(request.user, username) except models.User.DoesNotExist: return HttpResponseNotFound() - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - shelves = privacy_filter(request.user, user.shelf_set) # get the shelf and make sure the logged in user should be able to see it @@ -43,7 +38,7 @@ class Shelf(View): shelf = user.shelf_set.get(identifier=shelf_identifier) except models.Shelf.DoesNotExist: return HttpResponseNotFound() - if not object_visible_to_user(request.user, shelf): + if not shelf.visible_to_user(request.user): return HttpResponseNotFound() # this is a constructed "all books" view, with a fake "shelf" obj else: @@ -61,7 +56,7 @@ class Shelf(View): return ActivitypubResponse(shelf.to_activity(**request.GET)) paginated = Paginator( - shelf.books.order_by("-updated_date").all(), + shelf.books.order_by("-updated_date"), PAGE_LENGTH, ) @@ -70,7 +65,7 @@ class Shelf(View): "is_self": is_self, "shelves": shelves.all(), "shelf": shelf, - "books": paginated.page(page), + "books": paginated.get_page(request.GET.get("page")), } return TemplateResponse(request, "user/shelf.html", data) @@ -78,7 +73,7 @@ class Shelf(View): @method_decorator(login_required, name="dispatch") # pylint: disable=unused-argument def post(self, request, username, shelf_identifier): - """ edit a shelf """ + """edit a shelf""" try: shelf = request.user.shelf_set.get(identifier=shelf_identifier) except models.Shelf.DoesNotExist: @@ -99,7 +94,7 @@ class Shelf(View): @login_required @require_POST def create_shelf(request): - """ user generated shelves """ + """user generated shelves""" form = forms.ShelfForm(request.POST) if not form.is_valid(): return redirect(request.headers.get("Referer", "/")) @@ -111,7 +106,7 @@ def create_shelf(request): @login_required @require_POST def delete_shelf(request, shelf_id): - """ user generated shelves """ + """user generated shelves""" shelf = get_object_or_404(models.Shelf, id=shelf_id) if request.user != shelf.user or not shelf.editable: return HttpResponseBadRequest() @@ -123,7 +118,7 @@ def delete_shelf(request, shelf_id): @login_required @require_POST def shelve(request): - """ put a book on a user's shelf """ + """put a book on a user's shelf""" book = get_edition(request.POST.get("book")) desired_shelf = models.Shelf.objects.filter( @@ -182,7 +177,7 @@ def shelve(request): @login_required @require_POST def unshelve(request): - """ put a on a user's shelf """ + """put a on a user's shelf""" book = models.Edition.objects.get(id=request.POST["book"]) current_shelf = models.Shelf.objects.get(id=request.POST["shelf"]) @@ -192,6 +187,6 @@ def unshelve(request): # pylint: disable=unused-argument def handle_unshelve(book, shelf): - """ unshelve a book """ + """unshelve a book""" row = models.ShelfBook.objects.get(book=book, shelf=shelf) row.delete() diff --git a/bookwyrm/views/site.py b/bookwyrm/views/site.py index e5897660..46bdf722 100644 --- a/bookwyrm/views/site.py +++ b/bookwyrm/views/site.py @@ -15,16 +15,16 @@ from bookwyrm import emailing, forms, models name="dispatch", ) class Site(View): - """ manage things like the instance name """ + """manage things like the instance name""" def get(self, request): - """ edit form """ + """edit form""" site = models.SiteSettings.objects.get() data = {"site_form": forms.SiteForm(instance=site)} return TemplateResponse(request, "settings/site.html", data) def post(self, request): - """ edit the site settings """ + """edit the site settings""" site = models.SiteSettings.objects.get() form = forms.SiteForm(request.POST, request.FILES, instance=site) if not form.is_valid(): @@ -38,7 +38,7 @@ class Site(View): @login_required @permission_required("bookwyrm.edit_instance_settings", raise_exception=True) def email_preview(request): - """ for development, renders and example email template """ + """for development, renders and example email template""" template = request.GET.get("email") data = emailing.email_data() data["subject_path"] = "email/{}/subject.html".format(template) diff --git a/bookwyrm/views/status.py b/bookwyrm/views/status.py index f0119e0e..2295c8cc 100644 --- a/bookwyrm/views/status.py +++ b/bookwyrm/views/status.py @@ -19,16 +19,16 @@ from .reading import edit_readthrough # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class CreateStatus(View): - """ the view for *posting* """ + """the view for *posting*""" def get(self, request): - """ compose view (used for delete-and-redraft """ + """compose view (used for delete-and-redraft""" book = get_object_or_404(models.Edition, id=request.GET.get("book")) data = {"book": book} return TemplateResponse(request, "compose.html", data) def post(self, request, status_type): - """ create status of whatever type """ + """create status of whatever type""" status_type = status_type[0].upper() + status_type[1:] try: @@ -80,10 +80,10 @@ class CreateStatus(View): @method_decorator(login_required, name="dispatch") class DeleteStatus(View): - """ tombstone that bad boy """ + """tombstone that bad boy""" def post(self, request, status_id): - """ delete and tombstone a status """ + """delete and tombstone a status""" status = get_object_or_404(models.Status, id=status_id) # don't let people delete other people's statuses @@ -97,10 +97,10 @@ class DeleteStatus(View): @method_decorator(login_required, name="dispatch") class DeleteAndRedraft(View): - """ delete a status but let the user re-create it """ + """delete a status but let the user re-create it""" def post(self, request, status_id): - """ delete and tombstone a status """ + """delete and tombstone a status""" status = get_object_or_404( models.Status.objects.select_subclasses(), id=status_id ) @@ -130,7 +130,7 @@ class DeleteAndRedraft(View): def find_mentions(content): - """ detect @mentions in raw status content """ + """detect @mentions in raw status content""" if not content: return for match in re.finditer(regex.strict_username, content): @@ -148,7 +148,7 @@ def find_mentions(content): def format_links(content): - """ detect and format links """ + """detect and format links""" return re.sub( r'([^(href=")]|^|\()(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % regex.domain, r'\g<1>\g<3>', @@ -157,7 +157,7 @@ def format_links(content): def to_markdown(content): - """ catch links and convert to markdown """ + """catch links and convert to markdown""" content = markdown(content) content = format_links(content) # sanitize resulting html diff --git a/bookwyrm/views/tag.py b/bookwyrm/views/tag.py deleted file mode 100644 index a6bdf05a..00000000 --- a/bookwyrm/views/tag.py +++ /dev/null @@ -1,73 +0,0 @@ -""" tagging views""" -from django.contrib.auth.decorators import login_required -from django.shortcuts import get_object_or_404, redirect -from django.template.response import TemplateResponse -from django.utils.decorators import method_decorator -from django.views import View - -from bookwyrm import models -from bookwyrm.activitypub import ActivitypubResponse -from .helpers import is_api_request - - -# pylint: disable= no-self-use -class Tag(View): - """ tag page """ - - def get(self, request, tag_id): - """ see books related to a tag """ - tag_obj = get_object_or_404(models.Tag, identifier=tag_id) - - if is_api_request(request): - return ActivitypubResponse(tag_obj.to_activity(**request.GET)) - - books = models.Edition.objects.filter( - usertag__tag__identifier=tag_id - ).distinct() - data = { - "books": books, - "tag": tag_obj, - } - return TemplateResponse(request, "tag.html", data) - - -@method_decorator(login_required, name="dispatch") -class AddTag(View): - """ add a tag to a book """ - - def post(self, request): - """ tag a book """ - # I'm not using a form here because sometimes "name" is sent as a hidden - # field which doesn't validate - name = request.POST.get("name") - book_id = request.POST.get("book") - book = get_object_or_404(models.Edition, id=book_id) - tag_obj, _ = models.Tag.objects.get_or_create( - name=name, - ) - models.UserTag.objects.get_or_create( - user=request.user, - book=book, - tag=tag_obj, - ) - - return redirect("/book/%s" % book_id) - - -@method_decorator(login_required, name="dispatch") -class RemoveTag(View): - """ remove a user's tag from a book """ - - def post(self, request): - """ untag a book """ - name = request.POST.get("name") - tag_obj = get_object_or_404(models.Tag, name=name) - book_id = request.POST.get("book") - book = get_object_or_404(models.Edition, id=book_id) - - user_tag = get_object_or_404( - models.UserTag, tag=tag_obj, book=book, user=request.user - ) - user_tag.delete() - - return redirect("/book/%s" % book_id) diff --git a/bookwyrm/views/updates.py b/bookwyrm/views/updates.py index cc5fc419..34902272 100644 --- a/bookwyrm/views/updates.py +++ b/bookwyrm/views/updates.py @@ -7,7 +7,7 @@ from bookwyrm import activitystreams @login_required def get_notification_count(request): - """ any notifications waiting? """ + """any notifications waiting?""" return JsonResponse( { "count": request.user.notification_set.filter(read=False).count(), @@ -17,7 +17,7 @@ def get_notification_count(request): @login_required def get_unread_status_count(request, stream="home"): - """ any unread statuses for this feed? """ + """any unread statuses for this feed?""" stream = activitystreams.streams.get(stream) if not stream: return JsonResponse({}) diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index aba804d8..d394c1d7 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -17,15 +17,15 @@ from bookwyrm import forms, models from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.settings import PAGE_LENGTH from .helpers import get_user_from_username, is_api_request -from .helpers import is_blocked, privacy_filter, object_visible_to_user +from .helpers import is_blocked, privacy_filter # pylint: disable= no-self-use class User(View): - """ user profile page """ + """user profile page""" def get(self, request, username): - """ profile page for a user """ + """profile page for a user""" try: user = get_user_from_username(request.user, username) except models.User.DoesNotExist: @@ -40,11 +40,6 @@ class User(View): return ActivitypubResponse(user.to_activity()) # otherwise we're at a UI view - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - shelf_preview = [] # only show other shelves that should be visible @@ -80,14 +75,14 @@ class User(View): goal = models.AnnualGoal.objects.filter( user=user, year=timezone.now().year ).first() - if not object_visible_to_user(request.user, goal): + if goal and not goal.visible_to_user(request.user): goal = None data = { "user": user, "is_self": is_self, "shelves": shelf_preview, "shelf_count": shelves.count(), - "activities": paginated.page(page), + "activities": paginated.get_page(request.GET.get("page", 1)), "goal": goal, } @@ -95,10 +90,10 @@ class User(View): class Followers(View): - """ list of followers view """ + """list of followers view""" def get(self, request, username): - """ list of followers """ + """list of followers""" try: user = get_user_from_username(request.user, username) except models.User.DoesNotExist: @@ -111,19 +106,20 @@ class Followers(View): if is_api_request(request): return ActivitypubResponse(user.to_followers_activity(**request.GET)) + paginated = Paginator(user.followers.all(), PAGE_LENGTH) data = { "user": user, "is_self": request.user.id == user.id, - "followers": user.followers.all(), + "followers": paginated.page(request.GET.get("page", 1)), } return TemplateResponse(request, "user/followers.html", data) class Following(View): - """ list of following view """ + """list of following view""" def get(self, request, username): - """ list of followers """ + """list of followers""" try: user = get_user_from_username(request.user, username) except models.User.DoesNotExist: @@ -136,20 +132,21 @@ class Following(View): if is_api_request(request): return ActivitypubResponse(user.to_following_activity(**request.GET)) + paginated = Paginator(user.followers.all(), PAGE_LENGTH) data = { "user": user, "is_self": request.user.id == user.id, - "following": user.following.all(), + "following": paginated.page(request.GET.get("page", 1)), } return TemplateResponse(request, "user/following.html", data) @method_decorator(login_required, name="dispatch") class EditUser(View): - """ edit user view """ + """edit user view""" def get(self, request): - """ edit profile page for a user """ + """edit profile page for a user""" data = { "form": forms.EditUserForm(instance=request.user), "user": request.user, @@ -157,7 +154,7 @@ class EditUser(View): return TemplateResponse(request, "preferences/edit_user.html", data) def post(self, request): - """ les get fancy with images """ + """les get fancy with images""" form = forms.EditUserForm(request.POST, request.FILES, instance=request.user) if not form.is_valid(): data = {"form": form, "user": request.user} @@ -169,7 +166,7 @@ class EditUser(View): def save_user_form(form): - """ special handling for the user form """ + """special handling for the user form""" user = form.save(commit=False) if "avatar" in form.files: @@ -186,7 +183,7 @@ def save_user_form(form): def crop_avatar(image): - """ reduce the size and make an avatar square """ + """reduce the size and make an avatar square""" target_size = 120 width, height = image.size thumbnail_scale = ( diff --git a/bookwyrm/views/user_admin.py b/bookwyrm/views/user_admin.py index a8c155a1..9d08e930 100644 --- a/bookwyrm/views/user_admin.py +++ b/bookwyrm/views/user_admin.py @@ -6,7 +6,7 @@ from django.template.response import TemplateResponse from django.utils.decorators import method_decorator from django.views import View -from bookwyrm import models +from bookwyrm import forms, models from bookwyrm.settings import PAGE_LENGTH @@ -16,23 +16,22 @@ from bookwyrm.settings import PAGE_LENGTH permission_required("bookwyrm.moderate_users", raise_exception=True), name="dispatch", ) -class UserAdmin(View): - """ admin view of users on this server """ +class UserAdminList(View): + """admin view of users on this server""" def get(self, request): - """ list of users """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - + """list of users""" filters = {} server = request.GET.get("server") if server: - server = get_object_or_404(models.FederatedServer, id=server) + server = models.FederatedServer.objects.filter(server_name=server).first() filters["federated_server"] = server + filters["federated_server__isnull"] = False + username = request.GET.get("username") + if username: + filters["username__icontains"] = username - users = models.User.objects.filter(**filters).all() + users = models.User.objects.filter(**filters) sort = request.GET.get("sort", "-created_date") sort_fields = [ @@ -46,5 +45,33 @@ class UserAdmin(View): users = users.order_by(sort) paginated = Paginator(users, PAGE_LENGTH) - data = {"users": paginated.page(page), "sort": sort, "server": server} - return TemplateResponse(request, "settings/user_admin.html", data) + data = { + "users": paginated.get_page(request.GET.get("page")), + "sort": sort, + "server": server, + } + return TemplateResponse(request, "user_admin/user_admin.html", data) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required("bookwyrm.moderate_users", raise_exception=True), + name="dispatch", +) +class UserAdmin(View): + """moderate an individual user""" + + def get(self, request, user): + """user view""" + user = get_object_or_404(models.User, id=user) + data = {"user": user, "group_form": forms.UserGroupForm()} + return TemplateResponse(request, "user_admin/user.html", data) + + def post(self, request, user): + """update user group""" + user = get_object_or_404(models.User, id=user) + form = forms.UserGroupForm(request.POST, instance=user) + if form.is_valid(): + form.save() + data = {"user": user, "group_form": form} + return TemplateResponse(request, "user_admin/user.html", data) diff --git a/bookwyrm/views/wellknown.py b/bookwyrm/views/wellknown.py index 178d558e..2462c5a4 100644 --- a/bookwyrm/views/wellknown.py +++ b/bookwyrm/views/wellknown.py @@ -13,7 +13,7 @@ from bookwyrm.settings import DOMAIN, VERSION @require_GET def webfinger(request): - """ allow other servers to ask about a user """ + """allow other servers to ask about a user""" resource = request.GET.get("resource") if not resource or not resource.startswith("acct:"): return HttpResponseNotFound() @@ -40,7 +40,7 @@ def webfinger(request): @require_GET def nodeinfo_pointer(_): - """ direct servers to nodeinfo """ + """direct servers to nodeinfo""" return JsonResponse( { "links": [ @@ -55,7 +55,7 @@ def nodeinfo_pointer(_): @require_GET def nodeinfo(_): - """ basic info about the server """ + """basic info about the server""" status_count = models.Status.objects.filter(user__local=True).count() user_count = models.User.objects.filter(local=True).count() @@ -90,7 +90,7 @@ def nodeinfo(_): @require_GET def instance_info(_): - """ let's talk about your cool unique instance """ + """let's talk about your cool unique instance""" user_count = models.User.objects.filter(local=True).count() status_count = models.Status.objects.filter(user__local=True).count() @@ -116,12 +116,12 @@ def instance_info(_): @require_GET def peers(_): - """ list of federated servers this instance connects with """ + """list of federated servers this instance connects with""" names = models.FederatedServer.objects.values_list("server_name", flat=True) return JsonResponse(list(names), safe=False) @require_GET def host_meta(request): - """ meta of the host """ + """meta of the host""" return TemplateResponse(request, "host_meta.xml", {"DOMAIN": DOMAIN}) diff --git a/bw-dev b/bw-dev index b9c4b2a1..c2b63bc1 100755 --- a/bw-dev +++ b/bw-dev @@ -19,7 +19,6 @@ function clean { function runweb { docker-compose run --rm web "$@" - clean } function execdb { @@ -64,17 +63,16 @@ case "$CMD" in clean ;; makemigrations) - execweb python manage.py makemigrations "$@" + runweb python manage.py makemigrations "$@" ;; migrate) - execweb python manage.py rename_app fedireads bookwyrm - execweb python manage.py migrate "$@" + runweb python manage.py migrate "$@" ;; bash) - execweb bash + runweb bash ;; shell) - execweb python manage.py shell + runweb python manage.py shell ;; dbshell) execdb psql -U ${POSTGRES_USER} ${POSTGRES_DB} @@ -83,22 +81,19 @@ case "$CMD" in docker-compose restart celery_worker ;; test) - execweb coverage run --source='.' --omit="*/test*,celerywyrm*,bookwyrm/migrations/*" manage.py test "$@" + runweb coverage run --source='.' --omit="*/test*,celerywyrm*,bookwyrm/migrations/*" manage.py test "$@" ;; pytest) - execweb pytest --no-cov-on-fail "$@" - ;; - test_report) - execweb coverage report + runweb pytest --no-cov-on-fail "$@" ;; collectstatic) - execweb python manage.py collectstatic --no-input + runweb python manage.py collectstatic --no-input ;; makemessages) - execweb django-admin makemessages --no-wrap --ignore=venv3 $@ + runweb django-admin makemessages --no-wrap --ignore=venv $@ ;; compilemessages) - execweb django-admin compilemessages --ignore venv3 $@ + runweb django-admin compilemessages --ignore venv $@ ;; build) docker-compose build @@ -110,7 +105,7 @@ case "$CMD" in makeitblack ;; populate_streams) - execweb python manage.py populate_streams + runweb python manage.py populate_streams ;; *) echo "Unrecognised command. Try: build, clean, up, initdb, resetdb, makemigrations, migrate, bash, shell, dbshell, restart_celery, test, pytest, test_report, black, populate_feeds" diff --git a/celerywyrm/settings.py b/celerywyrm/settings.py index 7591163b..cd5b00ba 100644 --- a/celerywyrm/settings.py +++ b/celerywyrm/settings.py @@ -20,7 +20,8 @@ EMAIL_HOST = env("EMAIL_HOST") EMAIL_PORT = env("EMAIL_PORT") EMAIL_HOST_USER = env("EMAIL_HOST_USER") EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") -EMAIL_USE_TLS = env("EMAIL_USE_TLS") +EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS") +EMAIL_USE_SSL = env.bool("EMAIL_USE_SSL", False) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -148,7 +149,7 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.0/howto/static-files/ +# https://docs.djangoproject.com/en/3.1/howto/static-files/ STATIC_URL = "/static/" STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static")) diff --git a/certbot.sh b/certbot.sh new file mode 100644 index 00000000..6d2c3cd9 --- /dev/null +++ b/certbot.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +source .env; + +if [ "$CERTBOT_INIT" = "true" ] +then + certonly \ + --webroot \ + --webroot-path=/var/www/certbot \ + --email ${EMAIL} \ + --agree-tos \ + --no-eff-email \ + -d ${DOMAIN} \ + -d www.${DOMAIN} +else + renew \ + --webroot \ + --webroot-path \ + /var/www/certbot +fi diff --git a/docker-compose.yml b/docker-compose.yml index 3ee9037f..9324b224 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,8 @@ services: - pgdata:/var/lib/postgresql/data networks: - main + ports: + - 5432:5432 web: build: . env_file: .env @@ -38,20 +40,26 @@ services: - 8000:8000 redis_activity: image: redis + command: ["redis-server", "--appendonly", "yes"] env_file: .env ports: - 6378:6378 networks: - main restart: on-failure + volumes: + - redis_activity_data:/data redis_broker: image: redis + command: ["redis-server", "--appendonly", "yes"] env_file: .env ports: - 6379:6379 networks: - main restart: on-failure + volumes: + - redis_broker_data:/data celery_worker: env_file: .env build: . @@ -84,5 +92,7 @@ volumes: pgdata: static_volume: media_volume: + redis_broker_data: + redis_activity_data: networks: main: diff --git a/fr-dev b/fr-dev deleted file mode 120000 index 9947871e..00000000 --- a/fr-dev +++ /dev/null @@ -1 +0,0 @@ -bw-dev \ No newline at end of file diff --git a/instances.md b/instances.md deleted file mode 100644 index 570328b5..00000000 --- a/instances.md +++ /dev/null @@ -1,5 +0,0 @@ - -| name | url | admin contact | open registration | -| :--- | :-- | :------------ | :---------------- | -| bookwyrm.social | http://bookwyrm.social/ | mousereeve@riseup.net / @tripofmice@friend.camp | ❌ | -| wyrms.de | https://wyrms.de/ | wyrms@tofuwabo.hu / @tofuwabohu@subversive.zone | ❌ | diff --git a/locale/de_DE/LC_MESSAGES/django.po b/locale/de_DE/LC_MESSAGES/django.po index 962b9736..7acc3f96 100644 --- a/locale/de_DE/LC_MESSAGES/django.po +++ b/locale/de_DE/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-04-01 13:14-0700\n" +"POT-Creation-Date: 2021-04-26 09:56-0700\n" "PO-Revision-Date: 2021-03-02 17:19-0800\n" "Last-Translator: Mouse Reeve \n" "Language-Team: English \n" @@ -18,39 +18,70 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: bookwyrm/forms.py:226 +#: bookwyrm/forms.py:224 #, fuzzy #| msgid "A user with that username already exists." msgid "A user with this email already exists." msgstr "Dieser Benutzename ist bereits vergeben." -#: bookwyrm/forms.py:240 +#: bookwyrm/forms.py:238 msgid "One Day" msgstr "Ein Tag" -#: bookwyrm/forms.py:241 +#: bookwyrm/forms.py:239 msgid "One Week" msgstr "Eine Woche" -#: bookwyrm/forms.py:242 +#: bookwyrm/forms.py:240 msgid "One Month" msgstr "Ein Monat" -#: bookwyrm/forms.py:243 +#: bookwyrm/forms.py:241 msgid "Does Not Expire" msgstr "Läuft nicht aus" -#: bookwyrm/forms.py:248 +#: bookwyrm/forms.py:246 #, python-format msgid "%(count)d uses" msgstr "%(count)d Benutzungen" -#: bookwyrm/forms.py:251 +#: bookwyrm/forms.py:249 #, fuzzy #| msgid "Unlisted" msgid "Unlimited" msgstr "Ungelistet" +#: bookwyrm/forms.py:293 +msgid "List Order" +msgstr "" + +#: bookwyrm/forms.py:294 +#, fuzzy +#| msgid "Title" +msgid "Book Title" +msgstr "Titel" + +#: bookwyrm/forms.py:295 bookwyrm/templates/snippets/create_status_form.html:31 +#: bookwyrm/templates/user/shelf.html:80 bookwyrm/templates/user/shelf.html:110 +msgid "Rating" +msgstr "" + +#: bookwyrm/forms.py:297 bookwyrm/templates/lists/list.html:82 +msgid "Sort By" +msgstr "" + +#: bookwyrm/forms.py:301 +#, fuzzy +#| msgid "Started reading" +msgid "Ascending" +msgstr "Zu lesen angefangen" + +#: bookwyrm/forms.py:302 +#, fuzzy +#| msgid "Started reading" +msgid "Descending" +msgstr "Zu lesen angefangen" + #: bookwyrm/models/fields.py:24 #, python-format msgid "%(value)s is not a valid remote_id" @@ -61,7 +92,7 @@ msgstr "%(value)s ist keine gültige remote_id" msgid "%(value)s is not a valid username" msgstr "%(value)s ist kein gültiger Username" -#: bookwyrm/models/fields.py:165 bookwyrm/templates/layout.html:157 +#: bookwyrm/models/fields.py:165 bookwyrm/templates/layout.html:153 msgid "username" msgstr "Username" @@ -69,23 +100,23 @@ msgstr "Username" msgid "A user with that username already exists." msgstr "Dieser Benutzename ist bereits vergeben." -#: bookwyrm/settings.py:150 +#: bookwyrm/settings.py:152 msgid "English" msgstr "Englisch" -#: bookwyrm/settings.py:151 +#: bookwyrm/settings.py:153 msgid "German" msgstr "Deutsch" -#: bookwyrm/settings.py:152 +#: bookwyrm/settings.py:154 msgid "Spanish" msgstr "Spanisch" -#: bookwyrm/settings.py:153 +#: bookwyrm/settings.py:155 msgid "French" msgstr "Französisch" -#: bookwyrm/settings.py:154 +#: bookwyrm/settings.py:156 msgid "Simplified Chinese" msgstr "Vereinfachtes Chinesisch" @@ -122,82 +153,72 @@ msgstr "" msgid "Books by %(name)s" msgstr "Bücher von %(name)s" -#: bookwyrm/templates/book/book.html:21 +#: bookwyrm/templates/book/book.html:33 #: bookwyrm/templates/discover/large-book.html:12 #: bookwyrm/templates/discover/small-book.html:9 msgid "by" msgstr "von" -#: bookwyrm/templates/book/book.html:29 bookwyrm/templates/book/book.html:30 +#: bookwyrm/templates/book/book.html:41 bookwyrm/templates/book/book.html:42 msgid "Edit Book" msgstr "Buch editieren" -#: bookwyrm/templates/book/book.html:49 +#: bookwyrm/templates/book/book.html:61 #: bookwyrm/templates/book/cover_modal.html:5 msgid "Add cover" msgstr "Cover hinzufügen" -#: bookwyrm/templates/book/book.html:53 +#: bookwyrm/templates/book/book.html:65 #, fuzzy #| msgid "Failed to load" msgid "Failed to load cover" msgstr "Laden fehlgeschlagen" -#: bookwyrm/templates/book/book.html:62 -msgid "ISBN:" -msgstr "" - -#: bookwyrm/templates/book/book.html:69 -#: bookwyrm/templates/book/edit_book.html:211 -msgid "OCLC Number:" -msgstr "OCLC Nummer:" - -#: bookwyrm/templates/book/book.html:76 -#: bookwyrm/templates/book/edit_book.html:215 -msgid "ASIN:" -msgstr "" - -#: bookwyrm/templates/book/book.html:85 +#: bookwyrm/templates/book/book.html:82 msgid "View on OpenLibrary" msgstr "In OpenLibrary ansehen" -#: bookwyrm/templates/book/book.html:94 +#: bookwyrm/templates/book/book.html:102 #, python-format msgid "(%(review_count)s review)" msgid_plural "(%(review_count)s reviews)" msgstr[0] "(%(review_count)s Bewertung)" msgstr[1] "(%(review_count)s Bewertungen)" -#: bookwyrm/templates/book/book.html:100 +#: bookwyrm/templates/book/book.html:114 msgid "Add Description" msgstr "Beschreibung hinzufügen" -#: bookwyrm/templates/book/book.html:107 -#: bookwyrm/templates/book/edit_book.html:101 +#: bookwyrm/templates/book/book.html:121 +#: bookwyrm/templates/book/edit_book.html:107 #: bookwyrm/templates/lists/form.html:12 msgid "Description:" msgstr "Beschreibung:" -#: bookwyrm/templates/book/book.html:111 -#: bookwyrm/templates/book/edit_book.html:225 +#: bookwyrm/templates/book/book.html:125 +#: bookwyrm/templates/book/edit_book.html:240 #: bookwyrm/templates/edit_author.html:78 bookwyrm/templates/lists/form.html:42 #: bookwyrm/templates/preferences/edit_user.html:70 +#: bookwyrm/templates/settings/edit_server.html:68 +#: bookwyrm/templates/settings/federated_server.html:93 #: bookwyrm/templates/settings/site.html:93 -#: bookwyrm/templates/snippets/readthrough.html:65 +#: bookwyrm/templates/snippets/readthrough.html:75 #: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:42 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:42 #: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:34 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:38 msgid "Save" msgstr "Speichern" -#: bookwyrm/templates/book/book.html:112 bookwyrm/templates/book/book.html:161 +#: bookwyrm/templates/book/book.html:126 bookwyrm/templates/book/book.html:175 #: bookwyrm/templates/book/cover_modal.html:32 -#: bookwyrm/templates/book/edit_book.html:226 +#: bookwyrm/templates/book/edit_book.html:241 #: bookwyrm/templates/edit_author.html:79 -#: bookwyrm/templates/moderation/report_modal.html:32 +#: bookwyrm/templates/moderation/report_modal.html:34 +#: bookwyrm/templates/settings/federated_server.html:94 #: bookwyrm/templates/snippets/delete_readthrough_modal.html:17 #: bookwyrm/templates/snippets/goal_form.html:32 -#: bookwyrm/templates/snippets/readthrough.html:66 +#: bookwyrm/templates/snippets/readthrough.html:76 #: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:43 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:43 #: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:35 @@ -205,80 +226,124 @@ msgstr "Speichern" msgid "Cancel" msgstr "Abbrechen" -#: bookwyrm/templates/book/book.html:121 +#: bookwyrm/templates/book/book.html:135 #, fuzzy, python-format #| msgid "%(title)s by " msgid "%(count)s editions" msgstr "%(title)s von" -#: bookwyrm/templates/book/book.html:129 +#: bookwyrm/templates/book/book.html:143 #, fuzzy, python-format #| msgid "Direct Messages with %(username)s" msgid "This edition is on your %(shelf_name)s shelf." msgstr "Direktnachrichten mit %(username)s" -#: bookwyrm/templates/book/book.html:135 +#: bookwyrm/templates/book/book.html:149 #, fuzzy, python-format -#| msgid " added %(book_title)s to your list \"%(list_name)s\"" -msgid "A different edition of this book is on your %(shelf_name)s shelf." -msgstr "hat %(book_title)s zu deiner Liste \"%(list_name)s\" Hinzugefügt" +#| msgid "" +#| " added %(book_title)s to your list " +#| "\"%(list_name)s\"" +msgid "" +"A different edition of this book is on your %(shelf_name)s shelf." +msgstr "" +"hat %(book_title)s zu deiner Liste " +"\"%(list_name)s\" Hinzugefügt" -#: bookwyrm/templates/book/book.html:144 +#: bookwyrm/templates/book/book.html:158 msgid "Your reading activity" msgstr "Deine Leseaktivität" -#: bookwyrm/templates/book/book.html:146 +#: bookwyrm/templates/book/book.html:160 msgid "Add read dates" msgstr "Lesedaten hinzufügen" -#: bookwyrm/templates/book/book.html:151 +#: bookwyrm/templates/book/book.html:165 msgid "You don't have any reading activity for this book." msgstr "Du hast keine Leseaktivität für dieses Buch." -#: bookwyrm/templates/book/book.html:158 +#: bookwyrm/templates/book/book.html:172 msgid "Create" msgstr "Erstellen" -#: bookwyrm/templates/book/book.html:180 +#: bookwyrm/templates/book/book.html:194 msgid "Subjects" msgstr "Themen" -#: bookwyrm/templates/book/book.html:191 +#: bookwyrm/templates/book/book.html:206 msgid "Places" msgstr "Orte" -#: bookwyrm/templates/book/book.html:202 bookwyrm/templates/layout.html:64 +#: bookwyrm/templates/book/book.html:217 bookwyrm/templates/layout.html:65 #: bookwyrm/templates/lists/lists.html:5 bookwyrm/templates/lists/lists.html:12 #: bookwyrm/templates/search_results.html:91 #: bookwyrm/templates/user/user_layout.html:62 msgid "Lists" msgstr "Listen" -#: bookwyrm/templates/book/book.html:213 +#: bookwyrm/templates/book/book.html:228 #, fuzzy #| msgid "Go to list" msgid "Add to list" msgstr "Zur Liste" -#: bookwyrm/templates/book/book.html:223 +#: bookwyrm/templates/book/book.html:238 #: bookwyrm/templates/book/cover_modal.html:31 -#: bookwyrm/templates/lists/list.html:90 +#: bookwyrm/templates/lists/list.html:133 msgid "Add" msgstr "Hinzufügen" -#: bookwyrm/templates/book/book.html:251 +#: bookwyrm/templates/book/book.html:254 +#, fuzzy +#| msgid "Review" +msgid "Reviews" +msgstr "Bewerten" + +#: bookwyrm/templates/book/book.html:259 +#, fuzzy +#| msgid "Your shelves" +msgid "Your reviews" +msgstr "Deine Regale" + +#: bookwyrm/templates/book/book.html:265 +#, fuzzy +#| msgid "Your Account" +msgid "Your comments" +msgstr "Dein Account" + +#: bookwyrm/templates/book/book.html:271 +#, fuzzy +#| msgid "Your books" +msgid "Your quotes" +msgstr "Deine Bücher" + +#: bookwyrm/templates/book/book.html:305 msgid "rated it" msgstr "bewertet" +#: bookwyrm/templates/book/book_identifiers.html:8 +msgid "ISBN:" +msgstr "" + +#: bookwyrm/templates/book/book_identifiers.html:15 +#: bookwyrm/templates/book/edit_book.html:226 +msgid "OCLC Number:" +msgstr "OCLC Nummer:" + +#: bookwyrm/templates/book/book_identifiers.html:22 +#: bookwyrm/templates/book/edit_book.html:230 +msgid "ASIN:" +msgstr "" + #: bookwyrm/templates/book/cover_modal.html:17 -#: bookwyrm/templates/book/edit_book.html:163 +#: bookwyrm/templates/book/edit_book.html:178 #, fuzzy #| msgid "Add cover" msgid "Upload cover:" msgstr "Cover hinzufügen" #: bookwyrm/templates/book/cover_modal.html:23 -#: bookwyrm/templates/book/edit_book.html:169 +#: bookwyrm/templates/book/edit_book.html:184 msgid "Load cover from url:" msgstr "Cover von URL laden:" @@ -358,93 +423,93 @@ msgstr "Zurück" msgid "Metadata" msgstr "Metadaten" -#: bookwyrm/templates/book/edit_book.html:91 +#: bookwyrm/templates/book/edit_book.html:92 msgid "Title:" msgstr "Titel:" -#: bookwyrm/templates/book/edit_book.html:96 +#: bookwyrm/templates/book/edit_book.html:100 msgid "Subtitle:" msgstr "Untertitel:" -#: bookwyrm/templates/book/edit_book.html:106 +#: bookwyrm/templates/book/edit_book.html:113 msgid "Series:" msgstr "Serie:" -#: bookwyrm/templates/book/edit_book.html:111 +#: bookwyrm/templates/book/edit_book.html:120 msgid "Series number:" msgstr "Seriennummer:" -#: bookwyrm/templates/book/edit_book.html:117 +#: bookwyrm/templates/book/edit_book.html:126 #, fuzzy #| msgid "Published" msgid "Publisher:" msgstr "Veröffentlicht" -#: bookwyrm/templates/book/edit_book.html:119 +#: bookwyrm/templates/book/edit_book.html:128 msgid "Separate multiple publishers with commas." msgstr "Mehrere Herausgeber:innen durch Kommata trennen" -#: bookwyrm/templates/book/edit_book.html:125 +#: bookwyrm/templates/book/edit_book.html:135 msgid "First published date:" msgstr "Erstveröffentlichungsdatum:" -#: bookwyrm/templates/book/edit_book.html:130 +#: bookwyrm/templates/book/edit_book.html:143 msgid "Published date:" msgstr "Veröffentlichungsdatum:" -#: bookwyrm/templates/book/edit_book.html:137 +#: bookwyrm/templates/book/edit_book.html:152 #, fuzzy #| msgid "Author" msgid "Authors" msgstr "Autor*in" -#: bookwyrm/templates/book/edit_book.html:143 +#: bookwyrm/templates/book/edit_book.html:158 #, fuzzy, python-format #| msgid "Direct Messages with %(username)s" msgid "Remove %(name)s" msgstr "Direktnachrichten mit %(username)s" -#: bookwyrm/templates/book/edit_book.html:148 +#: bookwyrm/templates/book/edit_book.html:163 #, fuzzy #| msgid "Edit Author" msgid "Add Authors:" msgstr "Autor*in editieren" -#: bookwyrm/templates/book/edit_book.html:149 +#: bookwyrm/templates/book/edit_book.html:164 msgid "John Doe, Jane Smith" msgstr "" -#: bookwyrm/templates/book/edit_book.html:155 -#: bookwyrm/templates/user/shelf.html:75 +#: bookwyrm/templates/book/edit_book.html:170 +#: bookwyrm/templates/user/shelf.html:74 msgid "Cover" msgstr "" -#: bookwyrm/templates/book/edit_book.html:182 +#: bookwyrm/templates/book/edit_book.html:197 msgid "Physical Properties" msgstr "Physikalische Eigenschaften" -#: bookwyrm/templates/book/edit_book.html:183 +#: bookwyrm/templates/book/edit_book.html:198 #: bookwyrm/templates/book/format_filter.html:5 msgid "Format:" msgstr "" -#: bookwyrm/templates/book/edit_book.html:191 +#: bookwyrm/templates/book/edit_book.html:206 msgid "Pages:" msgstr "Seiten:" -#: bookwyrm/templates/book/edit_book.html:198 +#: bookwyrm/templates/book/edit_book.html:213 msgid "Book Identifiers" msgstr "Buchidentifikatoren" -#: bookwyrm/templates/book/edit_book.html:199 +#: bookwyrm/templates/book/edit_book.html:214 msgid "ISBN 13:" msgstr "" -#: bookwyrm/templates/book/edit_book.html:203 +#: bookwyrm/templates/book/edit_book.html:218 msgid "ISBN 10:" msgstr "" -#: bookwyrm/templates/book/edit_book.html:207 +#: bookwyrm/templates/book/edit_book.html:222 #: bookwyrm/templates/edit_author.html:59 msgid "Openlibrary key:" msgstr "" @@ -468,46 +533,57 @@ msgstr "" msgid "Language:" msgstr "" -#: bookwyrm/templates/book/publisher_info.html:6 +#: bookwyrm/templates/book/publisher_info.html:22 +#, python-format +msgid "%(format)s" +msgstr "" + +#: bookwyrm/templates/book/publisher_info.html:24 #, python-format msgid "%(format)s, %(pages)s pages" msgstr "%(format)s, %(pages)s Seiten" -#: bookwyrm/templates/book/publisher_info.html:8 +#: bookwyrm/templates/book/publisher_info.html:26 #, python-format msgid "%(pages)s pages" msgstr "%(pages)s Seiten" -#: bookwyrm/templates/book/publisher_info.html:13 +#: bookwyrm/templates/book/publisher_info.html:38 #, fuzzy, python-format #| msgid "%(pages)s pages" msgid "%(languages)s language" msgstr "%(pages)s Seiten" -#: bookwyrm/templates/book/publisher_info.html:18 +#: bookwyrm/templates/book/publisher_info.html:64 #, python-format msgid "Published %(date)s by %(publisher)s." msgstr "Am %(date)s von %(publisher)s veröffentlicht." -#: bookwyrm/templates/book/publisher_info.html:20 +#: bookwyrm/templates/book/publisher_info.html:66 #, fuzzy, python-format #| msgid "Published date:" msgid "Published %(date)s" msgstr "Veröffentlichungsdatum:" -#: bookwyrm/templates/book/publisher_info.html:22 +#: bookwyrm/templates/book/publisher_info.html:68 #, python-format msgid "Published by %(publisher)s." msgstr "Veröffentlicht von %(publisher)s." #: bookwyrm/templates/components/inline_form.html:8 #: bookwyrm/templates/components/modal.html:11 -#: bookwyrm/templates/feed/feed_layout.html:57 +#: bookwyrm/templates/feed/feed_layout.html:70 #: bookwyrm/templates/get_started/layout.html:19 #: bookwyrm/templates/get_started/layout.html:52 msgid "Close" msgstr "Schließen" +#: bookwyrm/templates/compose.html:5 bookwyrm/templates/compose.html:8 +#, fuzzy +#| msgid "Boost status" +msgid "Compose status" +msgstr "Status teilen" + #: bookwyrm/templates/directory/community_filter.html:5 #, fuzzy #| msgid "Comment" @@ -528,7 +604,7 @@ msgstr "Föderiert" #: bookwyrm/templates/directory/directory.html:6 #: bookwyrm/templates/directory/directory.html:11 -#: bookwyrm/templates/layout.html:97 +#: bookwyrm/templates/layout.html:93 msgid "Directory" msgstr "" @@ -538,9 +614,15 @@ msgstr "" #: bookwyrm/templates/directory/directory.html:26 #, fuzzy, python-format -#| msgid "You can set or change your reading goal any time from your profile page" -msgid "You can opt-out at any time in your profile settings." -msgstr "Du kannst dein Leseziel jederzeit auf deiner Profilseite setzen oder ändern." +#| msgid "" +#| "You can set or change your reading goal any time from your profile page" +msgid "" +"You can opt-out at any time in your profile settings." +msgstr "" +"Du kannst dein Leseziel jederzeit auf deiner Profilseite setzen oder ändern." #: bookwyrm/templates/directory/directory.html:31 #: bookwyrm/templates/snippets/goal_card.html:22 @@ -661,7 +743,7 @@ msgid "Email address:" msgstr "E-Mail Adresse" #: bookwyrm/templates/discover/landing_layout.html:70 -#: bookwyrm/templates/moderation/report_modal.html:31 +#: bookwyrm/templates/moderation/report_modal.html:33 msgid "Submit" msgstr "Absenden" @@ -716,7 +798,9 @@ msgstr "" #: bookwyrm/templates/email/html_layout.html:21 #, python-format -msgid "BookWyrm hosted on %(site_name)s" +msgid "" +"BookWyrm hosted on " +"%(site_name)s" msgstr "" #: bookwyrm/templates/email/html_layout.html:23 @@ -736,12 +820,16 @@ msgstr "" #: bookwyrm/templates/email/invite/html_content.html:15 #, python-format -msgid "Learn more about this instance." +msgid "" +"Learn more about this instance." msgstr "" #: bookwyrm/templates/email/invite/text_content.html:4 #, python-format -msgid "You're invited to join %(site_name)s! Click the link below to create an account." +msgid "" +"You're invited to join %(site_name)s! Click the link below to create an " +"account." msgstr "" #: bookwyrm/templates/email/invite/text_content.html:8 @@ -753,7 +841,9 @@ msgstr "Mehr über diese Seite" #: bookwyrm/templates/email/password_reset/html_content.html:6 #: bookwyrm/templates/email/password_reset/text_content.html:4 #, python-format -msgid "You requested to reset your %(site_name)s password. Click the link below to set a new password and log in to your account." +msgid "" +"You requested to reset your %(site_name)s password. Click the link below to " +"set a new password and log in to your account." msgstr "" #: bookwyrm/templates/email/password_reset/html_content.html:9 @@ -766,7 +856,8 @@ msgstr "Passwort zurücksetzen!" #: bookwyrm/templates/email/password_reset/html_content.html:13 #: bookwyrm/templates/email/password_reset/text_content.html:8 -msgid "If you didn't request to reset your password, you can ignore this email." +msgid "" +"If you didn't request to reset your password, you can ignore this email." msgstr "" #: bookwyrm/templates/email/password_reset/subject.html:2 @@ -781,7 +872,7 @@ msgid "Direct Messages with %(username)s" msgstr "Direktnachrichten mit %(username)s" #: bookwyrm/templates/feed/direct_messages.html:10 -#: bookwyrm/templates/layout.html:87 +#: bookwyrm/templates/layout.html:88 msgid "Direct Messages" msgstr "Direktnachrichten" @@ -816,6 +907,7 @@ msgid "Local" msgstr "Lokal" #: bookwyrm/templates/feed/feed.html:25 +#: bookwyrm/templates/settings/edit_server.html:40 msgid "Federated" msgstr "Föderiert" @@ -825,7 +917,8 @@ msgid "load 0 unread status(es)" msgstr "" #: bookwyrm/templates/feed/feed.html:48 -msgid "There aren't any activities right now! Try following a user to get started" +msgid "" +"There aren't any activities right now! Try following a user to get started" msgstr "Hier sind noch keine Aktivitäten! Folge anderen, um loszulegen" #: bookwyrm/templates/feed/feed.html:56 @@ -838,36 +931,38 @@ msgid "Updates" msgstr "" #: bookwyrm/templates/feed/feed_layout.html:11 -#: bookwyrm/templates/layout.html:58 +#: bookwyrm/templates/layout.html:59 #: bookwyrm/templates/user/books_header.html:3 msgid "Your books" msgstr "Deine Bücher" #: bookwyrm/templates/feed/feed_layout.html:13 -msgid "There are no books here right now! Try searching for a book to get started" -msgstr "Hier sind noch keine Bücher! Versuche nach Büchern zu suchen um loszulegen" +msgid "" +"There are no books here right now! Try searching for a book to get started" +msgstr "" +"Hier sind noch keine Bücher! Versuche nach Büchern zu suchen um loszulegen" -#: bookwyrm/templates/feed/feed_layout.html:23 +#: bookwyrm/templates/feed/feed_layout.html:24 #: bookwyrm/templates/user/shelf.html:28 #, fuzzy #| msgid "Read" msgid "To Read" msgstr "Auf der Leseliste" -#: bookwyrm/templates/feed/feed_layout.html:24 +#: bookwyrm/templates/feed/feed_layout.html:25 #: bookwyrm/templates/user/shelf.html:28 #, fuzzy #| msgid "Start reading" msgid "Currently Reading" msgstr "Gerade lesend" -#: bookwyrm/templates/feed/feed_layout.html:25 +#: bookwyrm/templates/feed/feed_layout.html:26 #: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:11 #: bookwyrm/templates/user/shelf.html:28 msgid "Read" msgstr "Gelesen" -#: bookwyrm/templates/feed/feed_layout.html:74 bookwyrm/templates/goal.html:26 +#: bookwyrm/templates/feed/feed_layout.html:88 bookwyrm/templates/goal.html:26 #: bookwyrm/templates/snippets/goal_card.html:6 #, python-format msgid "%(year)s Reading Goal" @@ -900,7 +995,7 @@ msgid "What are you reading?" msgstr "Zu lesen angefangen" #: bookwyrm/templates/get_started/books.html:9 -#: bookwyrm/templates/lists/list.html:58 +#: bookwyrm/templates/lists/list.html:101 msgid "Search for a book" msgstr "Nach einem Buch suchen" @@ -920,8 +1015,8 @@ msgstr "" #: bookwyrm/templates/get_started/books.html:17 #: bookwyrm/templates/get_started/users.html:18 #: bookwyrm/templates/get_started/users.html:19 -#: bookwyrm/templates/layout.html:37 bookwyrm/templates/layout.html:38 -#: bookwyrm/templates/lists/list.html:62 +#: bookwyrm/templates/layout.html:38 bookwyrm/templates/layout.html:39 +#: bookwyrm/templates/lists/list.html:105 msgid "Search" msgstr "Suche" @@ -938,7 +1033,7 @@ msgid "Popular on %(site_name)s" msgstr "Über %(site_name)s" #: bookwyrm/templates/get_started/books.html:51 -#: bookwyrm/templates/lists/list.html:75 +#: bookwyrm/templates/lists/list.html:118 msgid "No books found" msgstr "Keine Bücher gefunden" @@ -1016,7 +1111,9 @@ msgid "Show this account in suggested users:" msgstr "" #: bookwyrm/templates/get_started/profile.html:52 -msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users." +msgid "" +"Your account will show up in the directory, and may be recommended to other " +"BookWyrm users." msgstr "" #: bookwyrm/templates/get_started/users.html:11 @@ -1043,8 +1140,12 @@ msgstr "Ziel bearbeiten" #: bookwyrm/templates/goal.html:30 #: bookwyrm/templates/snippets/goal_card.html:13 #, python-format -msgid "Set a goal for how many books you'll finish reading in %(year)s, and track your progress throughout the year." -msgstr "Setze dir ein Ziel, wie viele Bücher du %(year)s lesen wirst und behalte deinen Fortschritt über's Jahr im Auge." +msgid "" +"Set a goal for how many books you'll finish reading in %(year)s, and track " +"your progress throughout the year." +msgstr "" +"Setze dir ein Ziel, wie viele Bücher du %(year)s lesen wirst und behalte " +"deinen Fortschritt über's Jahr im Auge." #: bookwyrm/templates/goal.html:39 #, python-format @@ -1062,7 +1163,7 @@ msgid "%(username)s's %(year)s Books" msgstr "%(username)ss %(year)s Bücher" #: bookwyrm/templates/import.html:5 bookwyrm/templates/import.html:9 -#: bookwyrm/templates/layout.html:102 +#: bookwyrm/templates/layout.html:98 msgid "Import Books" msgstr "Bücher importieren" @@ -1085,6 +1186,7 @@ msgid "Privacy setting for imported reviews:" msgstr "Datenschutzeinstellung für importierte Bewertungen" #: bookwyrm/templates/import.html:48 +#: bookwyrm/templates/settings/server_blocklist.html:64 msgid "Import" msgstr "Importieren" @@ -1127,8 +1229,12 @@ msgstr "Laden fehlgeschlagen" #: bookwyrm/templates/import_status.html:44 #, python-format -msgid "Jump to the bottom of the list to select the %(failed_count)s items which failed to import." -msgstr "Zum Ende der Liste springen, um die %(failed_count)s Einträge, deren Import fehlschlug, auszuwählen." +msgid "" +"Jump to the bottom of the list to select the %(failed_count)s items which " +"failed to import." +msgstr "" +"Zum Ende der Liste springen, um die %(failed_count)s Einträge, deren Import " +"fehlschlug, auszuwählen." #: bookwyrm/templates/import_status.html:79 msgid "Select all" @@ -1149,12 +1255,12 @@ msgstr "Buch" #: bookwyrm/templates/import_status.html:115 #: bookwyrm/templates/snippets/create_status_form.html:10 -#: bookwyrm/templates/user/shelf.html:76 +#: bookwyrm/templates/user/shelf.html:75 bookwyrm/templates/user/shelf.html:93 msgid "Title" msgstr "Titel" #: bookwyrm/templates/import_status.html:118 -#: bookwyrm/templates/user/shelf.html:77 +#: bookwyrm/templates/user/shelf.html:76 bookwyrm/templates/user/shelf.html:96 msgid "Author" msgstr "Autor*in" @@ -1191,91 +1297,94 @@ msgstr "Suchergebnisse für \"%(query)s\"" msgid "Matching Books" msgstr "Passende Bücher" -#: bookwyrm/templates/layout.html:33 +#: bookwyrm/templates/layout.html:34 msgid "Search for a book or user" msgstr "Suche nach Buch oder Benutzer*in" -#: bookwyrm/templates/layout.html:47 bookwyrm/templates/layout.html:48 +#: bookwyrm/templates/layout.html:48 bookwyrm/templates/layout.html:49 msgid "Main navigation menu" msgstr "Navigationshauptmenü" -#: bookwyrm/templates/layout.html:61 +#: bookwyrm/templates/layout.html:62 msgid "Feed" msgstr "" -#: bookwyrm/templates/layout.html:92 -#: bookwyrm/templates/preferences/preferences_layout.html:14 -msgid "Profile" -msgstr "Profil" - -#: bookwyrm/templates/layout.html:107 +#: bookwyrm/templates/layout.html:103 msgid "Settings" msgstr "Einstellungen" -#: bookwyrm/templates/layout.html:116 -#: bookwyrm/templates/settings/admin_layout.html:24 +#: bookwyrm/templates/layout.html:112 +#: bookwyrm/templates/settings/admin_layout.html:31 #: bookwyrm/templates/settings/manage_invite_requests.html:15 #: bookwyrm/templates/settings/manage_invites.html:3 #: bookwyrm/templates/settings/manage_invites.html:15 msgid "Invites" msgstr "Einladungen" -#: bookwyrm/templates/layout.html:123 +#: bookwyrm/templates/layout.html:119 msgid "Admin" msgstr "" -#: bookwyrm/templates/layout.html:130 +#: bookwyrm/templates/layout.html:126 msgid "Log out" msgstr "Abmelden" -#: bookwyrm/templates/layout.html:138 bookwyrm/templates/layout.html:139 +#: bookwyrm/templates/layout.html:134 bookwyrm/templates/layout.html:135 #: bookwyrm/templates/notifications.html:6 #: bookwyrm/templates/notifications.html:10 msgid "Notifications" msgstr "Benachrichtigungen" -#: bookwyrm/templates/layout.html:156 bookwyrm/templates/layout.html:160 +#: bookwyrm/templates/layout.html:152 bookwyrm/templates/layout.html:156 #: bookwyrm/templates/login.html:17 #: bookwyrm/templates/snippets/register_form.html:4 msgid "Username:" msgstr "" -#: bookwyrm/templates/layout.html:161 +#: bookwyrm/templates/layout.html:157 msgid "password" msgstr "Passwort" -#: bookwyrm/templates/layout.html:162 bookwyrm/templates/login.html:36 +#: bookwyrm/templates/layout.html:158 bookwyrm/templates/login.html:36 msgid "Forgot your password?" msgstr "Passwort vergessen?" -#: bookwyrm/templates/layout.html:165 bookwyrm/templates/login.html:10 +#: bookwyrm/templates/layout.html:161 bookwyrm/templates/login.html:10 #: bookwyrm/templates/login.html:33 msgid "Log in" msgstr "Anmelden" -#: bookwyrm/templates/layout.html:173 +#: bookwyrm/templates/layout.html:169 msgid "Join" msgstr "" -#: bookwyrm/templates/layout.html:196 +#: bookwyrm/templates/layout.html:195 msgid "About this server" msgstr "Über diesen Server" -#: bookwyrm/templates/layout.html:200 +#: bookwyrm/templates/layout.html:199 msgid "Contact site admin" msgstr "Admin kontaktieren" -#: bookwyrm/templates/layout.html:207 +#: bookwyrm/templates/layout.html:206 #, python-format -msgid "Support %(site_name)s on %(support_title)s" -msgstr "%(site_name)s auf %(support_title)s unterstützen" +msgid "" +"Support %(site_name)s on " +"%(support_title)s" +msgstr "" +"%(site_name)s auf " +"%(support_title)s unterstützen" -#: bookwyrm/templates/layout.html:211 -msgid "BookWyrm is open source software. You can contribute or report issues on GitHub." -msgstr "BookWyrm ist open source Software. Du kannst dich auf GitHub beteiligen oder etwas melden." +#: bookwyrm/templates/layout.html:210 +msgid "" +"BookWyrm is open source software. You can contribute or report issues on GitHub." +msgstr "" +"BookWyrm ist open source Software. Du kannst dich auf GitHub beteiligen oder etwas melden." #: bookwyrm/templates/lists/create_form.html:5 -#: bookwyrm/templates/lists/lists.html:19 +#: bookwyrm/templates/lists/lists.html:20 msgid "Create List" msgstr "Liste erstellen" @@ -1341,7 +1450,7 @@ msgid "Anyone can suggest books, subject to your approval" msgstr "Alle können Bücher vorschlagen, du kannst diese bestätigen" #: bookwyrm/templates/lists/form.html:31 -#: bookwyrm/templates/moderation/reports.html:24 +#: bookwyrm/templates/moderation/reports.html:25 msgid "Open" msgstr "Offen" @@ -1349,46 +1458,84 @@ msgstr "Offen" msgid "Anyone can add books to this list" msgstr "Alle können Bücher hinzufügen" -#: bookwyrm/templates/lists/list.html:17 +#: bookwyrm/templates/lists/list.html:19 +msgid "You successfully suggested a book for this list!" +msgstr "" + +#: bookwyrm/templates/lists/list.html:21 +#, fuzzy +#| msgid "Anyone can add books to this list" +msgid "You successfully added a book to this list!" +msgstr "Alle können Bücher hinzufügen" + +#: bookwyrm/templates/lists/list.html:27 msgid "This list is currently empty" msgstr "Diese Liste ist momentan leer" -#: bookwyrm/templates/lists/list.html:35 +#: bookwyrm/templates/lists/list.html:46 #, fuzzy, python-format #| msgid "Direct Messages with %(username)s" msgid "Added by %(username)s" msgstr "Direktnachrichten mit %(username)s" -#: bookwyrm/templates/lists/list.html:41 -#: bookwyrm/templates/snippets/shelf_selector.html:28 +#: bookwyrm/templates/lists/list.html:58 +#, fuzzy +#| msgid "Started" +msgid "Set" +msgstr "Gestartet" + +#: bookwyrm/templates/lists/list.html:61 +#, fuzzy +#| msgid "List curation:" +msgid "List position" +msgstr "Listenkuratierung:" + +#: bookwyrm/templates/lists/list.html:67 +#: bookwyrm/templates/snippets/shelf_selector.html:26 msgid "Remove" msgstr "Entfernen" -#: bookwyrm/templates/lists/list.html:54 +#: bookwyrm/templates/lists/list.html:80 bookwyrm/templates/lists/list.html:92 +#, fuzzy +#| msgid "Your Lists" +msgid "Sort List" +msgstr "Deine Listen" + +#: bookwyrm/templates/lists/list.html:86 +#, fuzzy +#| msgid "List curation:" +msgid "Direction" +msgstr "Listenkuratierung:" + +#: bookwyrm/templates/lists/list.html:97 msgid "Add Books" msgstr "Bücher hinzufügen" -#: bookwyrm/templates/lists/list.html:54 +#: bookwyrm/templates/lists/list.html:97 msgid "Suggest Books" msgstr "Bücher vorschlagen" -#: bookwyrm/templates/lists/list.html:63 +#: bookwyrm/templates/lists/list.html:106 msgid "search" msgstr "suchen" -#: bookwyrm/templates/lists/list.html:69 +#: bookwyrm/templates/lists/list.html:112 msgid "Clear search" msgstr "Suche leeren" -#: bookwyrm/templates/lists/list.html:74 +#: bookwyrm/templates/lists/list.html:117 #, python-format msgid "No books found matching the query \"%(query)s\"" msgstr "Keine passenden Bücher zu \"%(query)s\" gefunden" -#: bookwyrm/templates/lists/list.html:90 +#: bookwyrm/templates/lists/list.html:133 msgid "Suggest" msgstr "Vorschlagen" +#: bookwyrm/templates/lists/lists.html:14 bookwyrm/templates/user/lists.html:9 +msgid "Your Lists" +msgstr "Deine Listen" + #: bookwyrm/templates/login.html:4 msgid "Login" msgstr "" @@ -1406,65 +1553,41 @@ msgstr "Kontaktiere für eine Einladung eine*n Admin" msgid "More about this site" msgstr "Mehr über diese Seite" -#: bookwyrm/templates/moderation/report.html:5 #: bookwyrm/templates/moderation/report.html:6 +#: bookwyrm/templates/moderation/report.html:7 #: bookwyrm/templates/moderation/report_preview.html:6 #, python-format msgid "Report #%(report_id)s: %(username)s" msgstr "Meldung #%(report_id)s: %(username)s" -#: bookwyrm/templates/moderation/report.html:10 +#: bookwyrm/templates/moderation/report.html:11 msgid "Back to reports" msgstr "Zurück zu den Meldungen" -#: bookwyrm/templates/moderation/report.html:18 -#, fuzzy -#| msgid "Notifications" -msgid "Actions" -msgstr "Benachrichtigungen" - -#: bookwyrm/templates/moderation/report.html:19 -#, fuzzy -#| msgid "User Profile" -msgid "View user profile" -msgstr "Benutzerprofil" - -#: bookwyrm/templates/moderation/report.html:22 -#: bookwyrm/templates/snippets/status/status_options.html:25 -#: bookwyrm/templates/snippets/user_options.html:13 -msgid "Send direct message" -msgstr "Direktnachricht senden" - -#: bookwyrm/templates/moderation/report.html:27 -msgid "Deactivate user" -msgstr "Nutzer:in deaktivieren" - -#: bookwyrm/templates/moderation/report.html:29 -msgid "Reactivate user" -msgstr "Nutzer:in reaktivieren" - -#: bookwyrm/templates/moderation/report.html:36 +#: bookwyrm/templates/moderation/report.html:23 msgid "Moderator Comments" msgstr "Moderator:innenkommentare" -#: bookwyrm/templates/moderation/report.html:54 -#: bookwyrm/templates/snippets/create_status.html:12 -#: bookwyrm/templates/snippets/create_status_form.html:52 +#: bookwyrm/templates/moderation/report.html:41 +#: bookwyrm/templates/snippets/create_status.html:28 +#: bookwyrm/templates/snippets/create_status_form.html:53 msgid "Comment" msgstr "Kommentieren" -#: bookwyrm/templates/moderation/report.html:59 +#: bookwyrm/templates/moderation/report.html:46 #, fuzzy #| msgid "Delete status" msgid "Reported statuses" msgstr "Post löschen" -#: bookwyrm/templates/moderation/report.html:61 +#: bookwyrm/templates/moderation/report.html:48 msgid "No statuses reported" msgstr "Keine Beiträge gemeldet" -#: bookwyrm/templates/moderation/report.html:67 -msgid "Statuses has been deleted" +#: bookwyrm/templates/moderation/report.html:54 +#, fuzzy +#| msgid "Statuses has been deleted" +msgid "Status has been deleted" msgstr "Beiträge wurden gelöscht" #: bookwyrm/templates/moderation/report_modal.html:6 @@ -1473,12 +1596,13 @@ msgstr "Beiträge wurden gelöscht" msgid "Report @%(username)s" msgstr "Listen: %(username)s" -#: bookwyrm/templates/moderation/report_modal.html:21 +#: bookwyrm/templates/moderation/report_modal.html:23 #, python-format msgid "This report will be sent to %(site_name)s's moderators for review." -msgstr "Diese Meldung wird an die Moderator:innen von %(site_name)s weitergeletiet." +msgstr "" +"Diese Meldung wird an die Moderator:innen von %(site_name)s weitergeletiet." -#: bookwyrm/templates/moderation/report_modal.html:22 +#: bookwyrm/templates/moderation/report_modal.html:24 #, fuzzy #| msgid "More about this site" msgid "More info about this report:" @@ -1509,26 +1633,26 @@ msgid "Reports: %(server_name)s" msgstr "Listen: %(username)s" #: bookwyrm/templates/moderation/reports.html:8 -#: bookwyrm/templates/moderation/reports.html:16 -#: bookwyrm/templates/settings/admin_layout.html:28 +#: bookwyrm/templates/moderation/reports.html:17 +#: bookwyrm/templates/settings/admin_layout.html:35 #, fuzzy #| msgid "Recent Imports" msgid "Reports" msgstr "Aktuelle Importe" -#: bookwyrm/templates/moderation/reports.html:13 +#: bookwyrm/templates/moderation/reports.html:14 #, fuzzy, python-format #| msgid "Lists: %(username)s" msgid "Reports: %(server_name)s" msgstr "Listen: %(username)s" -#: bookwyrm/templates/moderation/reports.html:27 +#: bookwyrm/templates/moderation/reports.html:28 #, fuzzy #| msgid "Shelved" msgid "Resolved" msgstr "Ins Regal gestellt" -#: bookwyrm/templates/moderation/reports.html:34 +#: bookwyrm/templates/moderation/reports.html:37 #, fuzzy #| msgid "No books found" msgid "No reports found." @@ -1540,18 +1664,30 @@ msgstr "Benachrichtigungen löschen" #: bookwyrm/templates/notifications.html:53 #, python-format -msgid "favorited your review of %(book_title)s" -msgstr "hat deine Bewertung von %(book_title)s favorisiert" +msgid "" +"favorited your review of %(book_title)s" +msgstr "" +"hat deine Bewertung von %(book_title)s favorisiert" #: bookwyrm/templates/notifications.html:55 #, python-format -msgid "favorited your comment on %(book_title)s" -msgstr "hat deinen Kommentar zu %(book_title)s favorisiert" +msgid "" +"favorited your comment on %(book_title)s" +msgstr "" +"hat deinen Kommentar zu %(book_title)s favorisiert" #: bookwyrm/templates/notifications.html:57 #, python-format -msgid "favorited your quote from %(book_title)s" -msgstr " hat dein Zitat aus %(book_title)s favorisiert" +msgid "" +"favorited your quote from %(book_title)s" +msgstr "" +" hat dein Zitat aus %(book_title)s " +"favorisiert" #: bookwyrm/templates/notifications.html:59 #, python-format @@ -1560,18 +1696,30 @@ msgstr "hat deinen Status favorisiert" #: bookwyrm/templates/notifications.html:64 #, python-format -msgid "mentioned you in a review of %(book_title)s" -msgstr "hat dich in einer Bewertung von %(book_title)s erwähnt" +msgid "" +"mentioned you in a review of " +"%(book_title)s" +msgstr "" +"hat dich in einer Bewertung von " +"%(book_title)s erwähnt" #: bookwyrm/templates/notifications.html:66 #, python-format -msgid "mentioned you in a comment on %(book_title)s" -msgstr "hat dich in einem Kommentar zu %(book_title)s erwähnt" +msgid "" +"mentioned you in a comment on " +"%(book_title)s" +msgstr "" +"hat dich in einem Kommentar zu " +"%(book_title)s erwähnt" #: bookwyrm/templates/notifications.html:68 #, python-format -msgid "mentioned you in a quote from %(book_title)s" -msgstr "hat dich in einem Zitat von %(book_title)s erwähnt" +msgid "" +"mentioned you in a quote from " +"%(book_title)s" +msgstr "" +"hat dich in einem Zitat von %(book_title)s erwähnt" #: bookwyrm/templates/notifications.html:70 #, python-format @@ -1580,23 +1728,39 @@ msgstr "hat dich in einem Status erwähnt" #: bookwyrm/templates/notifications.html:75 #, python-format -msgid "replied to your review of %(book_title)s" -msgstr "hat auf deine Bewertung von %(book_title)s geantwortet " +msgid "" +"replied to your review of %(book_title)s" +msgstr "" +"hat auf deine Bewertung von %(book_title)s geantwortet " #: bookwyrm/templates/notifications.html:77 #, python-format -msgid "replied to your comment on %(book_title)s" -msgstr "hat auf deinen Kommentar zu %(book_title)s geantwortet" +msgid "" +"replied to your comment on %(book_title)s" +msgstr "" +"hat auf deinen Kommentar zu %(book_title)s geantwortet" #: bookwyrm/templates/notifications.html:79 #, python-format -msgid "replied to your quote from %(book_title)s" -msgstr "hat auf dein Zitat aus %(book_title)s geantwortet" +msgid "" +"replied to your quote from %(book_title)s" +msgstr "" +"hat auf dein Zitat aus %(book_title)s geantwortet" #: bookwyrm/templates/notifications.html:81 #, python-format -msgid "replied to your status" -msgstr "hat auf deinen Status geantwortet" +msgid "" +"replied to your status" +msgstr "" +"hat auf deinen Status geantwortet" #: bookwyrm/templates/notifications.html:85 msgid "followed you" @@ -1608,18 +1772,30 @@ msgstr "hat dir eine Folgeanfrage geschickt" #: bookwyrm/templates/notifications.html:94 #, python-format -msgid "boosted your review of %(book_title)s" -msgstr "hat deine Bewertung von %(book_title)s geteilt" +msgid "" +"boosted your review of %(book_title)s" +msgstr "" +"hat deine Bewertung von %(book_title)s geteilt" #: bookwyrm/templates/notifications.html:96 #, python-format -msgid "boosted your comment on%(book_title)s" -msgstr "hat deinen Kommentar zu%(book_title)s geteilt" +msgid "" +"boosted your comment on%(book_title)s" +msgstr "" +"hat deinen Kommentar zu%(book_title)s geteilt" #: bookwyrm/templates/notifications.html:98 #, python-format -msgid "boosted your quote from %(book_title)s" -msgstr "hat dein Zitat aus %(book_title)s geteilt" +msgid "" +"boosted your quote from %(book_title)s" +msgstr "" +"hat dein Zitat aus %(book_title)s " +"geteilt" #: bookwyrm/templates/notifications.html:100 #, python-format @@ -1628,13 +1804,21 @@ msgstr "hat deinen Status geteilt" #: bookwyrm/templates/notifications.html:104 #, python-format -msgid " added %(book_title)s to your list \"%(list_name)s\"" -msgstr "hat %(book_title)s zu deiner Liste \"%(list_name)s\" Hinzugefügt" +msgid "" +" added %(book_title)s to your list " +"\"%(list_name)s\"" +msgstr "" +"hat %(book_title)s zu deiner Liste " +"\"%(list_name)s\" Hinzugefügt" #: bookwyrm/templates/notifications.html:106 #, python-format -msgid " suggested adding %(book_title)s to your list \"%(list_name)s\"" -msgstr "hat %(book_title)s für deine Liste \"%(list_name)s\" vorgeschlagen" +msgid "" +" suggested adding %(book_title)s to " +"your list \"%(list_name)s\"" +msgstr "" +"hat %(book_title)s für deine Liste " +"\"%(list_name)s\" vorgeschlagen" #: bookwyrm/templates/notifications.html:110 #, python-format @@ -1657,7 +1841,9 @@ msgstr "Passwort bestätigen:" #: bookwyrm/templates/password_reset_request.html:14 msgid "A link to reset your password will be sent to your email address" -msgstr "Ein Link zum Zurücksetzen deines Passworts wird an deine Mailadresse geschickt" +msgstr "" +"Ein Link zum Zurücksetzen deines Passworts wird an deine Mailadresse " +"geschickt" #: bookwyrm/templates/password_reset_request.html:28 msgid "Reset password" @@ -1695,7 +1881,9 @@ msgstr "Angegebenes Leseziel im Feed anzeigen." #: bookwyrm/templates/preferences/edit_user.html:62 #, python-format -msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users." +msgid "" +"Your account will show up in the directory, and may " +"be recommended to other BookWyrm users." msgstr "" #: bookwyrm/templates/preferences/edit_user.html:65 @@ -1706,6 +1894,10 @@ msgstr "" msgid "Account" msgstr "" +#: bookwyrm/templates/preferences/preferences_layout.html:14 +msgid "Profile" +msgstr "Profil" + #: bookwyrm/templates/preferences/preferences_layout.html:20 msgid "Relationships" msgstr "Beziehungen" @@ -1735,133 +1927,215 @@ msgstr "Keine Liste für \"%(query)s\" gefunden" msgid "Administration" msgstr "" -#: bookwyrm/templates/settings/admin_layout.html:15 +#: bookwyrm/templates/settings/admin_layout.html:22 msgid "Manage Users" msgstr "Nutzer*innen verwalten" -#: bookwyrm/templates/settings/admin_layout.html:19 -#: bookwyrm/templates/settings/user_admin.html:3 -#: bookwyrm/templates/settings/user_admin.html:10 +#: bookwyrm/templates/settings/admin_layout.html:26 +#: bookwyrm/templates/user_admin/user_admin.html:3 +#: bookwyrm/templates/user_admin/user_admin.html:10 msgid "Users" msgstr "" -#: bookwyrm/templates/settings/admin_layout.html:32 +#: bookwyrm/templates/settings/admin_layout.html:39 #: bookwyrm/templates/settings/federation.html:3 #: bookwyrm/templates/settings/federation.html:5 msgid "Federated Servers" msgstr "Föderierende Server" -#: bookwyrm/templates/settings/admin_layout.html:37 +#: bookwyrm/templates/settings/admin_layout.html:44 msgid "Instance Settings" msgstr "Instanzeinstellungen" -#: bookwyrm/templates/settings/admin_layout.html:41 +#: bookwyrm/templates/settings/admin_layout.html:48 #: bookwyrm/templates/settings/site.html:4 #: bookwyrm/templates/settings/site.html:6 msgid "Site Settings" msgstr "Seiteneinstellungen" -#: bookwyrm/templates/settings/admin_layout.html:44 +#: bookwyrm/templates/settings/admin_layout.html:51 #: bookwyrm/templates/settings/site.html:13 msgid "Instance Info" msgstr "Instanzinformationen" -#: bookwyrm/templates/settings/admin_layout.html:45 +#: bookwyrm/templates/settings/admin_layout.html:52 #: bookwyrm/templates/settings/site.html:39 msgid "Images" msgstr "Bilder" -#: bookwyrm/templates/settings/admin_layout.html:46 +#: bookwyrm/templates/settings/admin_layout.html:53 #: bookwyrm/templates/settings/site.html:59 msgid "Footer Content" msgstr "Inhalt des Footers" -#: bookwyrm/templates/settings/admin_layout.html:47 +#: bookwyrm/templates/settings/admin_layout.html:54 #: bookwyrm/templates/settings/site.html:77 msgid "Registration" msgstr "Registrierung" -#: bookwyrm/templates/settings/federated_server.html:7 +#: bookwyrm/templates/settings/edit_server.html:3 +#: bookwyrm/templates/settings/edit_server.html:6 +#: bookwyrm/templates/settings/edit_server.html:20 +#: bookwyrm/templates/settings/federation.html:9 +#: bookwyrm/templates/settings/federation.html:10 +#: bookwyrm/templates/settings/server_blocklist.html:3 +#: bookwyrm/templates/settings/server_blocklist.html:20 +#, fuzzy +#| msgid "Add cover" +msgid "Add server" +msgstr "Cover hinzufügen" + +#: bookwyrm/templates/settings/edit_server.html:7 +#: bookwyrm/templates/settings/federated_server.html:12 +#: bookwyrm/templates/settings/server_blocklist.html:7 #, fuzzy #| msgid "Back to reports" msgid "Back to server list" msgstr "Zurück zu den Meldungen" -#: bookwyrm/templates/settings/federated_server.html:12 -msgid "Details" -msgstr "" - -#: bookwyrm/templates/settings/federated_server.html:15 -msgid "Software:" -msgstr "" - -#: bookwyrm/templates/settings/federated_server.html:19 +#: bookwyrm/templates/settings/edit_server.html:16 +#: bookwyrm/templates/settings/server_blocklist.html:16 #, fuzzy -#| msgid "Description:" -msgid "Version:" -msgstr "Beschreibung:" +#| msgid "Import book" +msgid "Import block list" +msgstr "Buch importieren" -#: bookwyrm/templates/settings/federated_server.html:23 +#: bookwyrm/templates/settings/edit_server.html:30 +#, fuzzy +#| msgid "Instance Name:" +msgid "Instance:" +msgstr "Instanzname" + +#: bookwyrm/templates/settings/edit_server.html:37 +#: bookwyrm/templates/settings/federated_server.html:29 +#: bookwyrm/templates/user_admin/user_info.html:34 #, fuzzy #| msgid "Import Status" msgid "Status:" msgstr "Importstatus" -#: bookwyrm/templates/settings/federated_server.html:30 +#: bookwyrm/templates/settings/edit_server.html:41 +#: bookwyrm/templates/settings/federated_server.html:9 +#, fuzzy +#| msgid "Blocked Users" +msgid "Blocked" +msgstr "Blockierte Nutzer*innen" + +#: bookwyrm/templates/settings/edit_server.html:48 +#: bookwyrm/templates/settings/federated_server.html:21 +#: bookwyrm/templates/user_admin/user_info.html:26 +msgid "Software:" +msgstr "" + +#: bookwyrm/templates/settings/edit_server.html:55 +#: bookwyrm/templates/settings/federated_server.html:25 +#: bookwyrm/templates/user_admin/user_info.html:30 +#, fuzzy +#| msgid "Description:" +msgid "Version:" +msgstr "Beschreibung:" + +#: bookwyrm/templates/settings/edit_server.html:64 +msgid "Notes:" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:18 +msgid "Details" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:36 #: bookwyrm/templates/user/user_layout.html:50 msgid "Activity" msgstr "Aktivität" -#: bookwyrm/templates/settings/federated_server.html:33 +#: bookwyrm/templates/settings/federated_server.html:39 msgid "Users:" msgstr "" -#: bookwyrm/templates/settings/federated_server.html:36 -#: bookwyrm/templates/settings/federated_server.html:43 +#: bookwyrm/templates/settings/federated_server.html:42 +#: bookwyrm/templates/settings/federated_server.html:49 msgid "View all" msgstr "" -#: bookwyrm/templates/settings/federated_server.html:40 +#: bookwyrm/templates/settings/federated_server.html:46 #, fuzzy #| msgid "Recent Imports" msgid "Reports:" msgstr "Aktuelle Importe" -#: bookwyrm/templates/settings/federated_server.html:47 +#: bookwyrm/templates/settings/federated_server.html:53 #, fuzzy #| msgid "followed you" msgid "Followed by us:" msgstr "folgt dir" -#: bookwyrm/templates/settings/federated_server.html:53 +#: bookwyrm/templates/settings/federated_server.html:59 #, fuzzy #| msgid "followed you" msgid "Followed by them:" msgstr "folgt dir" -#: bookwyrm/templates/settings/federated_server.html:59 +#: bookwyrm/templates/settings/federated_server.html:65 #, fuzzy #| msgid "Blocked Users" msgid "Blocked by us:" msgstr "Blockierte Nutzer*innen" -#: bookwyrm/templates/settings/federation.html:13 +#: bookwyrm/templates/settings/federated_server.html:77 +#: bookwyrm/templates/user_admin/user_info.html:39 +msgid "Notes" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:80 +#, fuzzy +#| msgid "Edit Book" +msgid "Edit" +msgstr "Buch editieren" + +#: bookwyrm/templates/settings/federated_server.html:100 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:3 +#, fuzzy +#| msgid "Notifications" +msgid "Actions" +msgstr "Benachrichtigungen" + +#: bookwyrm/templates/settings/federated_server.html:104 +#: bookwyrm/templates/snippets/block_button.html:5 +msgid "Block" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:105 +msgid "All users from this instance will be deactivated." +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:110 +#: bookwyrm/templates/snippets/block_button.html:10 +msgid "Un-block" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:111 +msgid "All users from this instance will be re-activated." +msgstr "" + +#: bookwyrm/templates/settings/federation.html:20 +#: bookwyrm/templates/user_admin/server_filter.html:5 msgid "Server name" msgstr "Servername" -#: bookwyrm/templates/settings/federation.html:17 +#: bookwyrm/templates/settings/federation.html:24 #, fuzzy #| msgid "Federated" msgid "Date federated" msgstr "Föderiert" -#: bookwyrm/templates/settings/federation.html:21 +#: bookwyrm/templates/settings/federation.html:28 msgid "Software" msgstr "" -#: bookwyrm/templates/settings/federation.html:24 -#: bookwyrm/templates/settings/manage_invite_requests.html:33 -#: bookwyrm/templates/settings/user_admin.html:32 +#: bookwyrm/templates/settings/federation.html:31 +#: bookwyrm/templates/settings/manage_invite_requests.html:44 +#: bookwyrm/templates/settings/status_filter.html:5 +#: bookwyrm/templates/user_admin/user_admin.html:34 msgid "Status" msgstr "" @@ -1878,63 +2152,74 @@ msgstr "Einladungen" msgid "Ignored Invite Requests" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:31 -msgid "Date" -msgstr "" +#: bookwyrm/templates/settings/manage_invite_requests.html:35 +#, fuzzy +#| msgid "Federated" +msgid "Date requested" +msgstr "Föderiert" -#: bookwyrm/templates/settings/manage_invite_requests.html:32 +#: bookwyrm/templates/settings/manage_invite_requests.html:39 +#, fuzzy +#| msgid "Accept" +msgid "Date accepted" +msgstr "Annehmen" + +#: bookwyrm/templates/settings/manage_invite_requests.html:42 msgid "Email" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:34 +#: bookwyrm/templates/settings/manage_invite_requests.html:47 #, fuzzy #| msgid "Notifications" msgid "Action" msgstr "Benachrichtigungen" -#: bookwyrm/templates/settings/manage_invite_requests.html:37 +#: bookwyrm/templates/settings/manage_invite_requests.html:50 #, fuzzy #| msgid "Follow Requests" msgid "No requests" msgstr "Folgeanfragen" -#: bookwyrm/templates/settings/manage_invite_requests.html:45 +#: bookwyrm/templates/settings/manage_invite_requests.html:59 +#: bookwyrm/templates/settings/status_filter.html:16 #, fuzzy #| msgid "Accept" msgid "Accepted" msgstr "Annehmen" -#: bookwyrm/templates/settings/manage_invite_requests.html:47 +#: bookwyrm/templates/settings/manage_invite_requests.html:61 +#: bookwyrm/templates/settings/status_filter.html:12 msgid "Sent" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:49 +#: bookwyrm/templates/settings/manage_invite_requests.html:63 +#: bookwyrm/templates/settings/status_filter.html:8 msgid "Requested" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:57 +#: bookwyrm/templates/settings/manage_invite_requests.html:73 msgid "Send invite" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:59 +#: bookwyrm/templates/settings/manage_invite_requests.html:75 msgid "Re-send invite" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:70 +#: bookwyrm/templates/settings/manage_invite_requests.html:95 msgid "Ignore" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:72 -msgid "Un-gnore" +#: bookwyrm/templates/settings/manage_invite_requests.html:97 +msgid "Un-ignore" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:83 +#: bookwyrm/templates/settings/manage_invite_requests.html:108 #, fuzzy #| msgid "Back to reports" msgid "Back to pending requests" msgstr "Zurück zu den Meldungen" -#: bookwyrm/templates/settings/manage_invite_requests.html:85 +#: bookwyrm/templates/settings/manage_invite_requests.html:110 msgid "View ignored requests" msgstr "" @@ -1974,6 +2259,27 @@ msgstr "Mal benutzt" msgid "No active invites" msgstr "Keine aktiven Einladungen" +#: bookwyrm/templates/settings/server_blocklist.html:6 +#, fuzzy +#| msgid "Import Books" +msgid "Import Blocklist" +msgstr "Bücher importieren" + +#: bookwyrm/templates/settings/server_blocklist.html:26 +#: bookwyrm/templates/snippets/goal_progress.html:5 +msgid "Success!" +msgstr "Erfolg!" + +#: bookwyrm/templates/settings/server_blocklist.html:30 +#, fuzzy +#| msgid "Successfully imported" +msgid "Successfully blocked:" +msgstr "Erfolgreich importiert" + +#: bookwyrm/templates/settings/server_blocklist.html:32 +msgid "Failed:" +msgstr "" + #: bookwyrm/templates/settings/site.html:15 msgid "Instance Name:" msgstr "Instanzname" @@ -2032,152 +2338,108 @@ msgstr "Folgeanfragen" msgid "Registration closed text:" msgstr "Registrierungen geschlossen text" -#: bookwyrm/templates/settings/user_admin.html:7 -#, python-format -msgid "Users: %(server_name)s" -msgstr "" - -#: bookwyrm/templates/settings/user_admin.html:20 +#: bookwyrm/templates/snippets/book_cover.html:20 +#: bookwyrm/templates/snippets/search_result_text.html:10 #, fuzzy -#| msgid "username" -msgid "Username" -msgstr "Username" +#| msgid "Add cover" +msgid "No cover" +msgstr "Cover hinzufügen" -#: bookwyrm/templates/settings/user_admin.html:24 -#, fuzzy -#| msgid "Added:" -msgid "Date Added" -msgstr "Hinzugefügt:" - -#: bookwyrm/templates/settings/user_admin.html:28 -msgid "Last Active" -msgstr "" - -#: bookwyrm/templates/settings/user_admin.html:36 -#, fuzzy -#| msgid "Remove" -msgid "Remote server" -msgstr "Entfernen" - -#: bookwyrm/templates/settings/user_admin.html:45 -#, fuzzy -#| msgid "Activity" -msgid "Active" -msgstr "Aktivität" - -#: bookwyrm/templates/settings/user_admin.html:45 -msgid "Inactive" -msgstr "" - -#: bookwyrm/templates/settings/user_admin.html:50 -msgid "Not set" -msgstr "" - -#: bookwyrm/templates/snippets/block_button.html:5 -msgid "Block" -msgstr "" - -#: bookwyrm/templates/snippets/block_button.html:10 -msgid "Un-block" -msgstr "" - -#: bookwyrm/templates/snippets/book_titleby.html:3 +#: bookwyrm/templates/snippets/book_titleby.html:4 #, python-format msgid "%(title)s by " msgstr "%(title)s von " #: bookwyrm/templates/snippets/boost_button.html:8 #: bookwyrm/templates/snippets/boost_button.html:9 -#: bookwyrm/templates/snippets/status/status_body.html:51 -#: bookwyrm/templates/snippets/status/status_body.html:52 -msgid "Boost status" -msgstr "Status teilen" +#, fuzzy +#| msgid "boosted" +msgid "Boost" +msgstr "teilt" +#: bookwyrm/templates/snippets/boost_button.html:15 #: bookwyrm/templates/snippets/boost_button.html:16 -#: bookwyrm/templates/snippets/boost_button.html:17 -msgid "Un-boost status" +#, fuzzy +#| msgid "Un-boost status" +msgid "Un-boost" msgstr "Teilen zurücknehmen" #: bookwyrm/templates/snippets/content_warning_field.html:3 msgid "Spoiler alert:" msgstr "Spoileralarm:" -#: bookwyrm/templates/snippets/content_warning_field.html:4 +#: bookwyrm/templates/snippets/content_warning_field.html:10 msgid "Spoilers ahead!" msgstr "Spoileralarm!" -#: bookwyrm/templates/snippets/create_status.html:9 +#: bookwyrm/templates/snippets/create_status.html:17 msgid "Review" msgstr "Bewerten" -#: bookwyrm/templates/snippets/create_status.html:15 +#: bookwyrm/templates/snippets/create_status.html:39 msgid "Quote" msgstr "Zitieren" -#: bookwyrm/templates/snippets/create_status_form.html:18 +#: bookwyrm/templates/snippets/create_status_form.html:20 #, fuzzy #| msgid "Comment" msgid "Comment:" msgstr "Kommentieren" -#: bookwyrm/templates/snippets/create_status_form.html:20 +#: bookwyrm/templates/snippets/create_status_form.html:22 #, fuzzy #| msgid "Quote" msgid "Quote:" msgstr "Zitieren" -#: bookwyrm/templates/snippets/create_status_form.html:22 +#: bookwyrm/templates/snippets/create_status_form.html:24 #, fuzzy #| msgid "Review" msgid "Review:" msgstr "Bewerten" -#: bookwyrm/templates/snippets/create_status_form.html:29 -#: bookwyrm/templates/user/shelf.html:81 -msgid "Rating" -msgstr "" +#: bookwyrm/templates/snippets/create_status_form.html:42 +#: bookwyrm/templates/snippets/status/layout.html:30 +#: bookwyrm/templates/snippets/status/layout.html:48 +#: bookwyrm/templates/snippets/status/layout.html:49 +msgid "Reply" +msgstr "Antwort" -#: bookwyrm/templates/snippets/create_status_form.html:31 -#: bookwyrm/templates/snippets/rate_action.html:14 -#: bookwyrm/templates/snippets/stars.html:3 -msgid "No rating" -msgstr "Kein Rating" - -#: bookwyrm/templates/snippets/create_status_form.html:64 +#: bookwyrm/templates/snippets/create_status_form.html:67 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:16 msgid "Progress:" msgstr "Fortschritt:" -#: bookwyrm/templates/snippets/create_status_form.html:71 +#: bookwyrm/templates/snippets/create_status_form.html:75 #: bookwyrm/templates/snippets/readthrough_form.html:22 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:30 msgid "pages" msgstr "Seiten" -#: bookwyrm/templates/snippets/create_status_form.html:72 +#: bookwyrm/templates/snippets/create_status_form.html:76 #: bookwyrm/templates/snippets/readthrough_form.html:23 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:31 msgid "percent" msgstr "Prozent" -#: bookwyrm/templates/snippets/create_status_form.html:77 +#: bookwyrm/templates/snippets/create_status_form.html:82 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:36 #, python-format msgid "of %(pages)s pages" msgstr "von %(pages)s Seiten" -#: bookwyrm/templates/snippets/create_status_form.html:89 +#: bookwyrm/templates/snippets/create_status_form.html:97 msgid "Include spoiler alert" msgstr "Spoileralarm aktivieren" -#: bookwyrm/templates/snippets/create_status_form.html:95 +#: bookwyrm/templates/snippets/create_status_form.html:104 #: bookwyrm/templates/snippets/privacy-icons.html:15 #: bookwyrm/templates/snippets/privacy-icons.html:16 #: bookwyrm/templates/snippets/privacy_select.html:19 msgid "Private" msgstr "Privat" -#: bookwyrm/templates/snippets/create_status_form.html:102 +#: bookwyrm/templates/snippets/create_status_form.html:115 msgid "Post" msgstr "Absenden" @@ -2187,24 +2449,28 @@ msgstr "Diese Lesedaten löschen?" #: bookwyrm/templates/snippets/delete_readthrough_modal.html:7 #, python-format -msgid "You are deleting this readthrough and its %(count)s associated progress updates." -msgstr "Du löscht diesen Leseforschritt und %(count)s zugehörige Fortschrittsupdates." +msgid "" +"You are deleting this readthrough and its %(count)s associated progress " +"updates." +msgstr "" +"Du löscht diesen Leseforschritt und %(count)s zugehörige Fortschrittsupdates." #: bookwyrm/templates/snippets/delete_readthrough_modal.html:15 #: bookwyrm/templates/snippets/follow_request_buttons.html:13 +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:391 msgid "Delete" msgstr "Löschen" #: bookwyrm/templates/snippets/fav_button.html:7 -#: bookwyrm/templates/snippets/fav_button.html:8 -#: bookwyrm/templates/snippets/status/status_body.html:55 -#: bookwyrm/templates/snippets/status/status_body.html:56 -msgid "Like status" -msgstr "Status favorisieren" +#: bookwyrm/templates/snippets/fav_button.html:9 +msgid "Like" +msgstr "" #: bookwyrm/templates/snippets/fav_button.html:15 #: bookwyrm/templates/snippets/fav_button.html:16 -msgid "Un-like status" +#, fuzzy +#| msgid "Un-like status" +msgid "Un-like" msgstr "Favorisieren zurücknehmen" #: bookwyrm/templates/snippets/filters_panel/filters_panel.html:7 @@ -2217,11 +2483,11 @@ msgstr "Weniger anzeigen" msgid "Hide filters" msgstr "" -#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:19 +#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:22 msgid "Apply filters" msgstr "" -#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:23 +#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:26 #, fuzzy #| msgid "Clear search" msgid "Clear filters" @@ -2245,6 +2511,11 @@ msgstr "Entfolgen" msgid "Accept" msgstr "Annehmen" +#: bookwyrm/templates/snippets/form_rate_stars.html:20 +#: bookwyrm/templates/snippets/stars.html:13 +msgid "No rating" +msgstr "Kein Rating" + #: bookwyrm/templates/snippets/generated_status/goal.html:1 #, python-format msgid "set a goal to read %(counter)s book in %(year)s" @@ -2255,15 +2526,19 @@ msgstr[1] "Setze das Ziel, %(year)s %(counter)s Bücher zu lesen" #: bookwyrm/templates/snippets/generated_status/rating.html:3 #, fuzzy, python-format #| msgid "%(title)s by " -msgid "Rated %(title)s: %(display_rating)s star" -msgid_plural "Rated %(title)s: %(display_rating)s stars" +msgid "" +"Rated %(title)s: %(display_rating)s star" +msgid_plural "" +"Rated %(title)s: %(display_rating)s stars" msgstr[0] "%(title)s von " msgstr[1] "%(title)s von " #: bookwyrm/templates/snippets/generated_status/review_pure_name.html:4 #, python-format -msgid "Review of \"%(book_title)s\" (%(display_rating)s star): %(review_title)s" -msgid_plural "Review of \"%(book_title)s\" (%(display_rating)s stars): %(review_title)s" +msgid "" +"Review of \"%(book_title)s\" (%(display_rating)s star): %(review_title)s" +msgid_plural "" +"Review of \"%(book_title)s\" (%(display_rating)s stars): %(review_title)s" msgstr[0] "" msgstr[1] "" @@ -2274,8 +2549,12 @@ msgstr "" #: bookwyrm/templates/snippets/goal_card.html:23 #, python-format -msgid "You can set or change your reading goal any time from your profile page" -msgstr "Du kannst dein Leseziel jederzeit auf deiner Profilseite setzen oder ändern." +msgid "" +"You can set or change your reading goal any time from your profile page" +msgstr "" +"Du kannst dein Leseziel jederzeit auf deiner Profilseite setzen oder ändern." #: bookwyrm/templates/snippets/goal_form.html:9 msgid "Reading goal:" @@ -2300,10 +2579,6 @@ msgstr "Posten" msgid "Set goal" msgstr "Ziel setzen" -#: bookwyrm/templates/snippets/goal_progress.html:5 -msgid "Success!" -msgstr "Erfolg!" - #: bookwyrm/templates/snippets/goal_progress.html:7 #, python-format msgid "%(percent)s%% complete!" @@ -2311,19 +2586,38 @@ msgstr "%(percent)s%% komplett!" #: bookwyrm/templates/snippets/goal_progress.html:10 #, python-format -msgid "You've read %(read_count)s of %(goal_count)s books." -msgstr "Du hast %(read_count)s von %(goal_count)s Büchern gelesen." +msgid "" +"You've read %(read_count)s of %(goal_count)s books." +msgstr "" +"Du hast %(read_count)s von %(goal_count)s Büchern " +"gelesen." #: bookwyrm/templates/snippets/goal_progress.html:12 #, python-format -msgid "%(username)s has read %(read_count)s of %(goal_count)s books." -msgstr "%(username)s hat %(read_count)s von %(goal_count)s Büchern gelesen." +msgid "" +"%(username)s has read %(read_count)s of %(goal_count)s " +"books." +msgstr "" +"%(username)s hat %(read_count)s von %(goal_count)s " +"Büchern gelesen." -#: bookwyrm/templates/snippets/pagination.html:7 +#: bookwyrm/templates/snippets/page_text.html:4 +#, fuzzy, python-format +#| msgid "of %(pages)s pages" +msgid "page %(page)s of %(total_pages)s" +msgstr "von %(pages)s Seiten" + +#: bookwyrm/templates/snippets/page_text.html:6 +#, fuzzy, python-format +#| msgid "%(pages)s pages" +msgid "page %(page)s" +msgstr "%(pages)s Seiten" + +#: bookwyrm/templates/snippets/pagination.html:12 msgid "Previous" msgstr "Zurück" -#: bookwyrm/templates/snippets/pagination.html:15 +#: bookwyrm/templates/snippets/pagination.html:23 msgid "Next" msgstr "Weiter" @@ -2356,7 +2650,7 @@ msgstr "Folgende" msgid "Leave a rating" msgstr "Raten" -#: bookwyrm/templates/snippets/rate_action.html:29 +#: bookwyrm/templates/snippets/rate_action.html:19 msgid "Rate" msgstr "" @@ -2364,28 +2658,28 @@ msgstr "" msgid "Progress Updates:" msgstr "Fortschrittsupdates:" -#: bookwyrm/templates/snippets/readthrough.html:12 +#: bookwyrm/templates/snippets/readthrough.html:14 msgid "finished" msgstr "Abgeschlossen" -#: bookwyrm/templates/snippets/readthrough.html:15 +#: bookwyrm/templates/snippets/readthrough.html:25 msgid "Show all updates" msgstr "Zeige alle Updates" -#: bookwyrm/templates/snippets/readthrough.html:31 +#: bookwyrm/templates/snippets/readthrough.html:41 msgid "Delete this progress update" msgstr "Dieses Fortschrittsupdate löschen" -#: bookwyrm/templates/snippets/readthrough.html:41 +#: bookwyrm/templates/snippets/readthrough.html:51 msgid "started" msgstr "Angefangen" -#: bookwyrm/templates/snippets/readthrough.html:47 -#: bookwyrm/templates/snippets/readthrough.html:61 +#: bookwyrm/templates/snippets/readthrough.html:57 +#: bookwyrm/templates/snippets/readthrough.html:71 msgid "Edit read dates" msgstr "Lesedaten bearbeiten" -#: bookwyrm/templates/snippets/readthrough.html:51 +#: bookwyrm/templates/snippets/readthrough.html:61 msgid "Delete these read dates" msgstr "Diese Lesedaten löschen" @@ -2415,31 +2709,25 @@ msgid "Report" msgstr "Importieren" #: bookwyrm/templates/snippets/rss_title.html:5 -#: bookwyrm/templates/snippets/status/status_header.html:11 +#: bookwyrm/templates/snippets/status/status_header.html:34 msgid "rated" msgstr "" #: bookwyrm/templates/snippets/rss_title.html:7 -#: bookwyrm/templates/snippets/status/status_header.html:13 +#: bookwyrm/templates/snippets/status/status_header.html:36 msgid "reviewed" msgstr "bewertete" #: bookwyrm/templates/snippets/rss_title.html:9 -#: bookwyrm/templates/snippets/status/status_header.html:15 +#: bookwyrm/templates/snippets/status/status_header.html:38 msgid "commented on" msgstr "kommentierte" #: bookwyrm/templates/snippets/rss_title.html:11 -#: bookwyrm/templates/snippets/status/status_header.html:17 +#: bookwyrm/templates/snippets/status/status_header.html:40 msgid "quoted" msgstr "zitierte" -#: bookwyrm/templates/snippets/search_result_text.html:10 -#, fuzzy -#| msgid "Add cover" -msgid "No cover" -msgstr "Cover hinzufügen" - #: bookwyrm/templates/snippets/search_result_text.html:22 #, python-format msgid "by %(author)s" @@ -2461,7 +2749,7 @@ msgid "Finish \"%(book_title)s\"" msgstr "\"%(book_title)s\" abschließen" #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:5 -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:35 +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:34 #, fuzzy #| msgid "Progress" msgid "Update progress" @@ -2484,7 +2772,7 @@ msgstr "Lesen abschließen" msgid "Want to read" msgstr "Auf Leseliste setzen" -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:48 +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:45 #, fuzzy, python-format #| msgid "Lists: %(username)s" msgid "Remove from %(name)s" @@ -2500,64 +2788,68 @@ msgstr "\"%(book_title)s\" beginnen" msgid "Want to Read \"%(book_title)s\"" msgstr "\"%(book_title)s\" auf Leseliste setzen" +#: bookwyrm/templates/snippets/status/content_status.html:67 +#: bookwyrm/templates/snippets/trimmed_text.html:14 +msgid "Show more" +msgstr "Mehr anzeigen" + +#: bookwyrm/templates/snippets/status/content_status.html:82 +#: bookwyrm/templates/snippets/trimmed_text.html:29 +msgid "Show less" +msgstr "Weniger anzeigen" + +#: bookwyrm/templates/snippets/status/content_status.html:112 +msgid "Open image in new window" +msgstr "Bild in neuem Fenster öffnen" + +#: bookwyrm/templates/snippets/status/layout.html:22 +#: bookwyrm/templates/snippets/status/status_options.html:17 +msgid "Delete status" +msgstr "Post löschen" + +#: bookwyrm/templates/snippets/status/layout.html:52 +#: bookwyrm/templates/snippets/status/layout.html:53 +msgid "Boost status" +msgstr "Status teilen" + +#: bookwyrm/templates/snippets/status/layout.html:56 +#: bookwyrm/templates/snippets/status/layout.html:57 +msgid "Like status" +msgstr "Status favorisieren" + #: bookwyrm/templates/snippets/status/status.html:9 msgid "boosted" msgstr "teilt" -#: bookwyrm/templates/snippets/status/status_body.html:27 -#: bookwyrm/templates/snippets/status/status_options.html:18 -msgid "Delete status" -msgstr "Post löschen" - -#: bookwyrm/templates/snippets/status/status_body.html:34 -#: bookwyrm/templates/snippets/status/status_body.html:47 -#: bookwyrm/templates/snippets/status/status_body.html:48 -msgid "Reply" -msgstr "Antwort" - -#: bookwyrm/templates/snippets/status/status_content.html:18 -#: bookwyrm/templates/snippets/trimmed_text.html:15 -msgid "Show more" -msgstr "Mehr anzeigen" - -#: bookwyrm/templates/snippets/status/status_content.html:25 -#: bookwyrm/templates/snippets/trimmed_text.html:25 -msgid "Show less" -msgstr "Weniger anzeigen" - -#: bookwyrm/templates/snippets/status/status_content.html:46 -msgid "Open image in new window" -msgstr "Bild in neuem Fenster öffnen" - -#: bookwyrm/templates/snippets/status/status_header.html:22 +#: bookwyrm/templates/snippets/status/status_header.html:44 #, fuzzy, python-format -#| msgid "Direct Messages with %(username)s" -msgid "replied to %(username)s's review" -msgstr "Direktnachrichten mit %(username)s" - -#: bookwyrm/templates/snippets/status/status_header.html:24 -#, fuzzy, python-format -#| msgid "replied to your status" -msgid "replied to %(username)s's comment" -msgstr "hat auf deinen Status geantwortet" - -#: bookwyrm/templates/snippets/status/status_header.html:26 -#, fuzzy, python-format -#| msgid "replied to your status" -msgid "replied to %(username)s's quote" -msgstr "hat auf deinen Status geantwortet" - -#: bookwyrm/templates/snippets/status/status_header.html:28 -#, fuzzy, python-format -#| msgid "replied to your status" -msgid "replied to %(username)s's status" -msgstr "hat auf deinen Status geantwortet" +#| msgid "" +#| "replied to your status" +msgid "" +"replied to %(username)s's status" +msgstr "" +"hat auf deinen Status geantwortet" #: bookwyrm/templates/snippets/status/status_options.html:7 #: bookwyrm/templates/snippets/user_options.html:7 msgid "More options" msgstr "Mehr Optionen" +#: bookwyrm/templates/snippets/status/status_options.html:26 +#, fuzzy +#| msgid "Delete these read dates" +msgid "Delete & re-draft" +msgstr "Diese Lesedaten löschen" + +#: bookwyrm/templates/snippets/status/status_options.html:35 +#: bookwyrm/templates/snippets/user_options.html:13 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:6 +msgid "Send direct message" +msgstr "Direktnachricht senden" + #: bookwyrm/templates/snippets/switch_edition_button.html:5 msgid "Switch to this edition" msgstr "Zu dieser Edition wechseln" @@ -2574,19 +2866,6 @@ msgstr "Zu lesen angefangen" msgid "Sorted descending" msgstr "Zu lesen angefangen" -#: bookwyrm/templates/snippets/tag.html:14 -msgid "Remove tag" -msgstr "Tag entfernen" - -#: bookwyrm/templates/snippets/tag.html:18 -msgid "Add tag" -msgstr "Tag hinzufügen" - -#: bookwyrm/templates/tag.html:9 -#, python-format -msgid "Books tagged \"%(tag.name)s\"" -msgstr "Mit \"%(tag.name)s\" markierte Bücher" - #: bookwyrm/templates/user/books_header.html:5 #, fuzzy, python-format #| msgid "%(username)s's %(year)s Books" @@ -2626,10 +2905,6 @@ msgstr "Folgend" msgid "%(username)s isn't following any users" msgstr "%(username)s folgt niemandem" -#: bookwyrm/templates/user/lists.html:9 -msgid "Your Lists" -msgstr "Deine Listen" - #: bookwyrm/templates/user/lists.html:11 #, python-format msgid "Lists: %(username)s" @@ -2639,7 +2914,7 @@ msgstr "Listen: %(username)s" msgid "Create list" msgstr "Liste Erstellen" -#: bookwyrm/templates/user/shelf.html:24 bookwyrm/views/shelf.py:56 +#: bookwyrm/templates/user/shelf.html:24 bookwyrm/views/shelf.py:51 #, fuzzy #| msgid "books" msgid "All books" @@ -2653,23 +2928,23 @@ msgstr "Regal erstellen" msgid "Edit shelf" msgstr "Regal bearbeiten" -#: bookwyrm/templates/user/shelf.html:78 +#: bookwyrm/templates/user/shelf.html:77 bookwyrm/templates/user/shelf.html:99 msgid "Shelved" msgstr "Ins Regal gestellt" -#: bookwyrm/templates/user/shelf.html:79 +#: bookwyrm/templates/user/shelf.html:78 bookwyrm/templates/user/shelf.html:103 msgid "Started" msgstr "Gestartet" -#: bookwyrm/templates/user/shelf.html:80 +#: bookwyrm/templates/user/shelf.html:79 bookwyrm/templates/user/shelf.html:106 msgid "Finished" msgstr "Abgeschlossen" -#: bookwyrm/templates/user/shelf.html:127 +#: bookwyrm/templates/user/shelf.html:132 msgid "This shelf is empty." msgstr "Dieses Regal ist leer." -#: bookwyrm/templates/user/shelf.html:133 +#: bookwyrm/templates/user/shelf.html:138 msgid "Delete shelf" msgstr "Regal löschen" @@ -2735,6 +3010,87 @@ msgstr[1] "%(counter)s Folgende" msgid "%(counter)s following" msgstr "Folgt %(counter)s" +#: bookwyrm/templates/user_admin/user.html:11 +#, fuzzy +#| msgid "Back to reports" +msgid "Back to users" +msgstr "Zurück zu den Meldungen" + +#: bookwyrm/templates/user_admin/user_admin.html:7 +#, python-format +msgid "Users: %(server_name)s" +msgstr "" + +#: bookwyrm/templates/user_admin/user_admin.html:22 +#: bookwyrm/templates/user_admin/username_filter.html:5 +#, fuzzy +#| msgid "username" +msgid "Username" +msgstr "Username" + +#: bookwyrm/templates/user_admin/user_admin.html:26 +#, fuzzy +#| msgid "Added:" +msgid "Date Added" +msgstr "Hinzugefügt:" + +#: bookwyrm/templates/user_admin/user_admin.html:30 +msgid "Last Active" +msgstr "" + +#: bookwyrm/templates/user_admin/user_admin.html:38 +#, fuzzy +#| msgid "Remove" +msgid "Remote server" +msgstr "Entfernen" + +#: bookwyrm/templates/user_admin/user_admin.html:47 +#, fuzzy +#| msgid "Activity" +msgid "Active" +msgstr "Aktivität" + +#: bookwyrm/templates/user_admin/user_admin.html:47 +msgid "Inactive" +msgstr "" + +#: bookwyrm/templates/user_admin/user_admin.html:52 +#: bookwyrm/templates/user_admin/user_info.html:49 +msgid "Not set" +msgstr "" + +#: bookwyrm/templates/user_admin/user_info.html:5 +msgid "User details" +msgstr "" + +#: bookwyrm/templates/user_admin/user_info.html:14 +#, fuzzy +#| msgid "User Profile" +msgid "View user profile" +msgstr "Benutzerprofil" + +#: bookwyrm/templates/user_admin/user_info.html:20 +#, fuzzy +#| msgid "Instance Settings" +msgid "Instance details" +msgstr "Instanzeinstellungen" + +#: bookwyrm/templates/user_admin/user_info.html:46 +msgid "View instance" +msgstr "" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:11 +msgid "Suspend user" +msgstr "" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:13 +msgid "Un-suspend user" +msgstr "" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:21 +msgid "Access level:" +msgstr "" + #: bookwyrm/views/password.py:32 #, fuzzy #| msgid "A user with that username already exists." @@ -2746,6 +3102,1443 @@ msgstr "Dieser Benutzename ist bereits vergeben." msgid "A password reset link sent to %s" msgstr "" +#: venv3/lib/python3.8/site-packages/_pytest/config/argparsing.py:442 +#, python-format +msgid "ambiguous option: %(option)s could match %(matches)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/contrib/messages/apps.py:7 +#, fuzzy +#| msgid "All messages" +msgid "Messages" +msgstr "Alle Nachrichten" + +#: venv3/lib/python3.8/site-packages/django/contrib/sitemaps/apps.py:7 +msgid "Site Maps" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/contrib/staticfiles/apps.py:9 +msgid "Static Files" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/contrib/syndication/apps.py:7 +msgid "Syndication" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/paginator.py:45 +msgid "That page number is not an integer" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/paginator.py:47 +msgid "That page number is less than 1" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/paginator.py:52 +msgid "That page contains no results" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:32 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid value." +msgstr "E-Mail Adresse" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:103 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:659 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid URL." +msgstr "E-Mail Adresse" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:155 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid integer." +msgstr "E-Mail Adresse" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:166 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid email address." +msgstr "E-Mail Adresse" + +#. Translators: "letters" means latin letters: a-z and A-Z. +#: venv3/lib/python3.8/site-packages/django/core/validators.py:240 +msgid "" +"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:247 +msgid "" +"Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " +"hyphens." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:256 +#: venv3/lib/python3.8/site-packages/django/core/validators.py:276 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid IPv4 address." +msgstr "E-Mail Adresse" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:261 +#: venv3/lib/python3.8/site-packages/django/core/validators.py:277 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid IPv6 address." +msgstr "E-Mail Adresse" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:271 +#: venv3/lib/python3.8/site-packages/django/core/validators.py:275 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid IPv4 or IPv6 address." +msgstr "E-Mail Adresse" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:305 +msgid "Enter only digits separated by commas." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:311 +#, python-format +msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:343 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:352 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:362 +#, python-format +msgid "" +"Ensure this value has at least %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at least %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:377 +#, python-format +msgid "" +"Ensure this value has at most %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at most %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:396 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:291 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:326 +#, fuzzy +#| msgid "Series number:" +msgid "Enter a number." +msgstr "Seriennummer:" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:398 +#, python-format +msgid "Ensure that there are no more than %(max)s digit in total." +msgid_plural "Ensure that there are no more than %(max)s digits in total." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:403 +#, python-format +msgid "Ensure that there are no more than %(max)s decimal place." +msgid_plural "Ensure that there are no more than %(max)s decimal places." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:408 +#, python-format +msgid "" +"Ensure that there are no more than %(max)s digit before the decimal point." +msgid_plural "" +"Ensure that there are no more than %(max)s digits before the decimal point." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:470 +#, python-format +msgid "" +"File extension “%(extension)s” is not allowed. Allowed extensions are: " +"%(allowed_extensions)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:522 +msgid "Null characters are not allowed." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/base.py:1181 +#: venv3/lib/python3.8/site-packages/django/forms/models.py:759 +msgid "and" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/base.py:1183 +#, fuzzy, python-format +#| msgid "A user with that username already exists." +msgid "%(model_name)s with this %(field_labels)s already exists." +msgstr "Dieser Benutzename ist bereits vergeben." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:104 +#, fuzzy, python-format +#| msgid "%(value)s is not a valid remote_id" +msgid "Value %(value)r is not a valid choice." +msgstr "%(value)s ist keine gültige remote_id" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:105 +#, fuzzy +#| msgid "This shelf is empty." +msgid "This field cannot be null." +msgstr "Dieses Regal ist leer." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:106 +msgid "This field cannot be blank." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:107 +#, fuzzy, python-format +#| msgid "A user with that username already exists." +msgid "%(model_name)s with this %(field_label)s already exists." +msgstr "Dieser Benutzename ist bereits vergeben." + +#. Translators: The 'lookup_type' is one of 'date', 'year' or 'month'. +#. Eg: "Title must be unique for pub_date year" +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:111 +#, python-format +msgid "" +"%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:130 +#, python-format +msgid "Field of type: %(field_type)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:937 +#, python-format +msgid "“%(value)s” value must be either True or False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:938 +#, python-format +msgid "“%(value)s” value must be either True, False, or None." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:940 +msgid "Boolean (Either True or False)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:981 +#, python-format +msgid "String (up to %(max_length)s)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1045 +#, fuzzy +#| msgid "No active invites" +msgid "Comma-separated integers" +msgstr "Keine aktiven Einladungen" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1094 +#, python-format +msgid "" +"“%(value)s” value has an invalid date format. It must be in YYYY-MM-DD " +"format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1096 +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1239 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD) but it is an invalid " +"date." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1099 +msgid "Date (without time)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1237 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." +"uuuuuu]][TZ] format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1241 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" +"[TZ]) but it is an invalid date/time." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1245 +msgid "Date (with time)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1393 +#, fuzzy, python-format +#| msgid "%(value)s is not a valid username" +msgid "“%(value)s” value must be a decimal number." +msgstr "%(value)s ist kein gültiger Username" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1395 +#, fuzzy +#| msgid "Series number:" +msgid "Decimal number" +msgstr "Seriennummer:" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1534 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in [DD] [[HH:]MM:]ss[." +"uuuuuu] format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1537 +#, fuzzy +#| msgid "List curation:" +msgid "Duration" +msgstr "Listenkuratierung:" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1587 +#, fuzzy +#| msgid "Email address:" +msgid "Email address" +msgstr "E-Mail Adresse" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1610 +msgid "File path" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1676 +#, fuzzy, python-format +#| msgid "%(value)s is not a valid username" +msgid "“%(value)s” value must be a float." +msgstr "%(value)s ist kein gültiger Username" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1678 +msgid "Floating point number" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1716 +#, python-format +msgid "“%(value)s” value must be an integer." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1718 +msgid "Integer" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1801 +#, fuzzy +#| msgid "No active invites" +msgid "Big (8 byte) integer" +msgstr "Keine aktiven Einladungen" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1817 +#, fuzzy +#| msgid "Email address:" +msgid "IPv4 address" +msgstr "E-Mail Adresse" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1848 +#, fuzzy +#| msgid "Email address:" +msgid "IP address" +msgstr "E-Mail Adresse" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1928 +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1929 +#, python-format +msgid "“%(value)s” value must be either None, True or False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1931 +msgid "Boolean (Either True, False or None)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1966 +#, fuzzy +#| msgid "No active invites" +msgid "Positive integer" +msgstr "Keine aktiven Einladungen" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1979 +#, fuzzy +#| msgid "No active invites" +msgid "Positive small integer" +msgstr "Keine aktiven Einladungen" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1993 +#, python-format +msgid "Slug (up to %(max_length)s)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2025 +msgid "Small integer" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2032 +msgid "Text" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2060 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] " +"format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2062 +#, python-format +msgid "" +"“%(value)s” value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an " +"invalid time." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2065 +msgid "Time" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2191 +msgid "URL" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2213 +msgid "Raw binary data" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2278 +#, fuzzy, python-format +#| msgid "%(value)s is not a valid username" +msgid "“%(value)s” is not a valid UUID." +msgstr "%(value)s ist kein gültiger Username" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2280 +msgid "Universally unique identifier" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/files.py:221 +msgid "File" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/files.py:360 +#, fuzzy +#| msgid "Images" +msgid "Image" +msgstr "Bilder" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:778 +#, python-format +msgid "%(model)s instance with %(field)s %(value)r does not exist." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:780 +msgid "Foreign Key (type determined by related field)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1012 +#, fuzzy +#| msgid "Relationships" +msgid "One-to-one relationship" +msgstr "Beziehungen" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1066 +#, fuzzy, python-format +#| msgid "Relationships" +msgid "%(from)s-%(to)s relationship" +msgstr "Beziehungen" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1067 +#, fuzzy, python-format +#| msgid "Relationships" +msgid "%(from)s-%(to)s relationships" +msgstr "Beziehungen" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1109 +#, fuzzy +#| msgid "Relationships" +msgid "Many-to-many relationship" +msgstr "Beziehungen" + +#. Translators: If found as last label character, these punctuation +#. characters will prevent the default label_suffix to be appended to the label +#: venv3/lib/python3.8/site-packages/django/forms/boundfield.py:149 +msgid ":?.!" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:53 +#, fuzzy +#| msgid "This shelf is empty." +msgid "This field is required." +msgstr "Dieses Regal ist leer." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:246 +#, fuzzy +#| msgid "Series number:" +msgid "Enter a whole number." +msgstr "Seriennummer:" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:397 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:1127 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid date." +msgstr "E-Mail Adresse" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:421 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:1128 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid time." +msgstr "E-Mail Adresse" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:443 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid date/time." +msgstr "E-Mail Adresse" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:472 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid duration." +msgstr "E-Mail Adresse" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:473 +#, python-brace-format +msgid "The number of days must be between {min_days} and {max_days}." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:533 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:534 +msgid "No file was submitted." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:535 +#, fuzzy +#| msgid "This shelf is empty." +msgid "The submitted file is empty." +msgstr "Dieses Regal ist leer." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:537 +#, python-format +msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." +msgid_plural "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:540 +msgid "Please either submit a file or check the clear checkbox, not both." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:601 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:763 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:853 +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1275 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:854 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:969 +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1274 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a list of values." +msgstr "E-Mail Adresse" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:970 +msgid "Enter a complete value." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:1186 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid UUID." +msgstr "E-Mail Adresse" + +#. Translators: This is the default suffix added to form field labels +#: venv3/lib/python3.8/site-packages/django/forms/forms.py:81 +msgid ":" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/forms.py:207 +#, python-format +msgid "(Hidden field %(name)s) %(error)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:93 +msgid "ManagementForm data is missing or has been tampered with" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:345 +#, python-format +msgid "Please submit %d or fewer forms." +msgid_plural "Please submit %d or fewer forms." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:352 +#, python-format +msgid "Please submit %d or more forms." +msgid_plural "Please submit %d or more forms." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:379 +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:386 +msgid "Order" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:754 +#, python-format +msgid "Please correct the duplicate data for %(field)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:758 +#, python-format +msgid "Please correct the duplicate data for %(field)s, which must be unique." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:764 +#, python-format +msgid "" +"Please correct the duplicate data for %(field_name)s which must be unique " +"for the %(lookup)s in %(date_field)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:773 +msgid "Please correct the duplicate values below." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1094 +msgid "The inline value did not match the parent instance." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1161 +msgid "Select a valid choice. That choice is not one of the available choices." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1277 +#, fuzzy, python-format +#| msgid "%(value)s is not a valid username" +msgid "“%(pk)s” is not a valid value." +msgstr "%(value)s ist kein gültiger Username" + +#: venv3/lib/python3.8/site-packages/django/forms/utils.py:162 +#, python-format +msgid "" +"%(datetime)s couldn’t be interpreted in time zone %(current_timezone)s; it " +"may be ambiguous or it may not exist." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:395 +msgid "Clear" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:396 +#, fuzzy +#| msgid "Start reading" +msgid "Currently" +msgstr "Gerade lesend" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:397 +#, fuzzy +#| msgid "Change shelf" +msgid "Change" +msgstr "Regal wechseln" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:709 +msgid "Unknown" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:710 +msgid "Yes" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:711 +msgid "No" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:788 +msgid "yes,no,maybe" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:817 +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:834 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:836 +#, python-format +msgid "%s KB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:838 +#, python-format +msgid "%s MB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:840 +#, python-format +msgid "%s GB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:842 +#, python-format +msgid "%s TB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:844 +#, python-format +msgid "%s PB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:62 +msgid "p.m." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:63 +msgid "a.m." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:68 +msgid "PM" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:69 +msgid "AM" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:150 +msgid "midnight" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:152 +msgid "noon" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:295 +msgid "Monday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:296 +msgid "Tuesday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:297 +msgid "Wednesday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:298 +msgid "Thursday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:299 +msgid "Friday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:7 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:300 +msgid "Saturday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:7 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:301 +msgid "Sunday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Mon" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Tue" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Wed" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Thu" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Fri" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:11 +#, fuzzy +#| msgid "Started" +msgid "Sat" +msgstr "Gestartet" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:11 +msgid "Sun" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:281 +msgid "January" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:282 +msgid "February" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:283 +#, fuzzy +#| msgid "Search" +msgid "March" +msgstr "Suche" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:284 +msgid "April" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:285 +msgid "May" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:286 +msgid "June" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:287 +msgid "July" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:288 +msgid "August" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:289 +#, fuzzy +#| msgid "Series number:" +msgid "September" +msgstr "Seriennummer:" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:290 +msgid "October" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:291 +msgid "November" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:16 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:292 +#, fuzzy +#| msgid "Series number:" +msgid "December" +msgstr "Seriennummer:" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "jan" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "feb" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "mar" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "apr" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "may" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "jun" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "jul" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "aug" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "sep" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "oct" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "nov" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "dec" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:23 +msgctxt "abbrev. month" +msgid "Jan." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:24 +msgctxt "abbrev. month" +msgid "Feb." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:25 +#, fuzzy +#| msgid "Search" +msgctxt "abbrev. month" +msgid "March" +msgstr "Suche" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:26 +msgctxt "abbrev. month" +msgid "April" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:27 +msgctxt "abbrev. month" +msgid "May" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:28 +msgctxt "abbrev. month" +msgid "June" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:29 +msgctxt "abbrev. month" +msgid "July" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:30 +msgctxt "abbrev. month" +msgid "Aug." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:31 +msgctxt "abbrev. month" +msgid "Sept." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:32 +msgctxt "abbrev. month" +msgid "Oct." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:33 +msgctxt "abbrev. month" +msgid "Nov." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:34 +msgctxt "abbrev. month" +msgid "Dec." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:37 +msgctxt "alt. month" +msgid "January" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:38 +msgctxt "alt. month" +msgid "February" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:39 +#, fuzzy +#| msgid "Search" +msgctxt "alt. month" +msgid "March" +msgstr "Suche" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:40 +msgctxt "alt. month" +msgid "April" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:41 +msgctxt "alt. month" +msgid "May" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:42 +msgctxt "alt. month" +msgid "June" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:43 +msgctxt "alt. month" +msgid "July" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:44 +msgctxt "alt. month" +msgid "August" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:45 +#, fuzzy +#| msgid "Series number:" +msgctxt "alt. month" +msgid "September" +msgstr "Seriennummer:" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:46 +msgctxt "alt. month" +msgid "October" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:47 +msgctxt "alt. month" +msgid "November" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:48 +#, fuzzy +#| msgid "Series number:" +msgctxt "alt. month" +msgid "December" +msgstr "Seriennummer:" + +#: venv3/lib/python3.8/site-packages/django/utils/ipv6.py:8 +#, fuzzy +#| msgid "Email address:" +msgid "This is not a valid IPv6 address." +msgstr "E-Mail Adresse" + +#: venv3/lib/python3.8/site-packages/django/utils/text.py:69 +#, python-format +msgctxt "String to return when truncating text" +msgid "%(truncated_text)s…" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/text.py:235 +msgid "or" +msgstr "" + +#. Translators: This string is used as a separator between list elements +#: venv3/lib/python3.8/site-packages/django/utils/text.py:254 +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:83 +msgid ", " +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:9 +#, python-format +msgid "%d year" +msgid_plural "%d years" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:10 +#, python-format +msgid "%d month" +msgid_plural "%d months" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:11 +#, python-format +msgid "%d week" +msgid_plural "%d weeks" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:12 +#, python-format +msgid "%d day" +msgid_plural "%d days" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:13 +#, python-format +msgid "%d hour" +msgid_plural "%d hours" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:14 +#, python-format +msgid "%d minute" +msgid_plural "%d minutes" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:72 +msgid "0 minutes" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:110 +msgid "Forbidden" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:111 +msgid "CSRF verification failed. Request aborted." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:115 +msgid "" +"You are seeing this message because this HTTPS site requires a “Referer " +"header” to be sent by your Web browser, but none was sent. This header is " +"required for security reasons, to ensure that your browser is not being " +"hijacked by third parties." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:120 +msgid "" +"If you have configured your browser to disable “Referer” headers, please re-" +"enable them, at least for this site, or for HTTPS connections, or for “same-" +"origin” requests." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:124 +msgid "" +"If you are using the tag or " +"including the “Referrer-Policy: no-referrer” header, please remove them. The " +"CSRF protection requires the “Referer” header to do strict referer checking. " +"If you’re concerned about privacy, use alternatives like for links to third-party sites." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:132 +msgid "" +"You are seeing this message because this site requires a CSRF cookie when " +"submitting forms. This cookie is required for security reasons, to ensure " +"that your browser is not being hijacked by third parties." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:137 +msgid "" +"If you have configured your browser to disable cookies, please re-enable " +"them, at least for this site, or for “same-origin” requests." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:142 +msgid "More information is available with DEBUG=True." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:41 +msgid "No year specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:61 +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:111 +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:208 +msgid "Date out of range" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:90 +msgid "No month specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:142 +msgid "No day specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:188 +msgid "No week specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:338 +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:367 +#, python-format +msgid "No %(verbose_name_plural)s available" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:589 +#, python-format +msgid "" +"Future %(verbose_name_plural)s not available because %(class_name)s." +"allow_future is False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:623 +#, python-format +msgid "Invalid date string “%(datestr)s” given format “%(format)s”" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/detail.py:54 +#, fuzzy, python-format +#| msgid "No books found matching the query \"%(query)s\"" +msgid "No %(verbose_name)s found matching the query" +msgstr "Keine passenden Bücher zu \"%(query)s\" gefunden" + +#: venv3/lib/python3.8/site-packages/django/views/generic/list.py:67 +msgid "Page is not “last”, nor can it be converted to an int." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/list.py:72 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/list.py:154 +#, python-format +msgid "Empty list and “%(class_name)s.allow_empty” is False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/static.py:40 +msgid "Directory indexes are not allowed here." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/static.py:42 +#, fuzzy, python-format +#| msgid "%(value)s is not a valid username" +msgid "“%(path)s” does not exist" +msgstr "%(value)s ist kein gültiger Username" + +#: venv3/lib/python3.8/site-packages/django/views/static.py:80 +#, python-format +msgid "Index of %(directory)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:7 +msgid "Django: the Web framework for perfectionists with deadlines." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:346 +#, python-format +msgid "" +"View release notes for Django %(version)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:368 +msgid "The install worked successfully! Congratulations!" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:369 +#, python-format +msgid "" +"You are seeing this page because DEBUG=True is in your settings file and you have not configured any " +"URLs." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:384 +msgid "Django Documentation" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:385 +msgid "Topics, references, & how-to’s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:396 +msgid "Tutorial: A Polling App" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:397 +msgid "Get started with Django" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:408 +#, fuzzy +#| msgid "Comment" +msgid "Django Community" +msgstr "Kommentieren" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:409 +msgid "Connect, get help, or contribute" +msgstr "" + +#: venv3/lib/python3.8/site-packages/kombu/transport/qpid.py:1301 +#, python-format +msgid "Attempting to connect to qpid with SASL mechanism %s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/kombu/transport/qpid.py:1306 +#, python-format +msgid "Connected to qpid with SASL mechanism %s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/kombu/transport/qpid.py:1324 +#, python-format +msgid "Unable to connect to qpid with SASL mechanism %s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:371 +msgid "1 second ago" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:377 +msgid "1 minute ago" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:382 +msgid "1 hour ago" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:385 +#, python-format +msgid "%(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:387 +msgid "yesterday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:387 +#, python-format +msgid "yesterday at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:389 +#, python-format +msgid "%(weekday)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:389 +#, python-format +msgid "%(weekday)s at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:392 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:445 +#, python-format +msgid "%(month_name)s %(day)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:394 +#, python-format +msgid "%(month_name)s %(day)s at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:399 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:401 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:439 +#, python-format +msgid "%(weekday)s, %(month_name)s %(day)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:462 +#, python-format +msgid "%(commas)s and %(last)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:68 +msgctxt "law" +msgid "right" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:69 +msgctxt "good" +msgid "right" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:71 +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:74 +msgctxt "organization" +msgid "club" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:76 +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:77 +msgctxt "stick" +msgid "club" +msgstr "" + +#~ msgid "Deactivate user" +#~ msgstr "Nutzer:in deaktivieren" + +#~ msgid "Reactivate user" +#~ msgstr "Nutzer:in reaktivieren" + +#, fuzzy, python-format +#~| msgid "Direct Messages with %(username)s" +#~ msgid "" +#~ "replied to %(username)s's review" +#~ msgstr "Direktnachrichten mit %(username)s" + +#, fuzzy, python-format +#~| msgid "" +#~| "replied to your status" +#~ msgid "" +#~ "replied to %(username)s's comment" +#~ msgstr "" +#~ "hat auf deinen Status geantwortet" + +#, fuzzy, python-format +#~| msgid "" +#~| "replied to your status" +#~ msgid "" +#~ "replied to %(username)s's quote" +#~ msgstr "" +#~ "hat auf deinen Status geantwortet" + +#~ msgid "Remove tag" +#~ msgstr "Tag entfernen" + +#~ msgid "Add tag" +#~ msgstr "Tag hinzufügen" + +#, python-format +#~ msgid "Books tagged \"%(tag.name)s\"" +#~ msgstr "Mit \"%(tag.name)s\" markierte Bücher" + #, fuzzy #~| msgid "Started" #~ msgid "Getting Started" @@ -2756,9 +4549,6 @@ msgstr "" #~ msgid "No users were found for \"%(query)s\"" #~ msgstr "Keine Nutzer*innen für \"%(query)s\" gefunden" -#~ msgid "Your shelves" -#~ msgstr "Deine Regale" - #~ msgid "Your lists" #~ msgstr "Deine Listen" @@ -2799,134 +4589,6 @@ msgstr "" #~ msgid "Send follow request" #~ msgstr "Folgeanfrage senden" -#, fuzzy -#~| msgid "All messages" -#~ msgid "Messages" -#~ msgstr "Alle Nachrichten" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "Enter a valid email address." -#~ msgstr "E-Mail Adresse" - -#, fuzzy -#~| msgid "Series number:" -#~ msgid "Enter a number." -#~ msgstr "Seriennummer:" - -#, fuzzy -#~| msgid "%(value)s is not a valid remote_id" -#~ msgid "Value %(value)r is not a valid choice." -#~ msgstr "%(value)s ist keine gültige remote_id" - -#, fuzzy -#~| msgid "Series number:" -#~ msgid "Decimal number" -#~ msgstr "Seriennummer:" - -#, fuzzy -#~| msgid "List curation:" -#~ msgid "Duration" -#~ msgstr "Listenkuratierung:" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "Email address" -#~ msgstr "E-Mail Adresse" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "IPv4 address" -#~ msgstr "E-Mail Adresse" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "IP address" -#~ msgstr "E-Mail Adresse" - -#, fuzzy -#~| msgid "No active invites" -#~ msgid "Positive integer" -#~ msgstr "Keine aktiven Einladungen" - -#, fuzzy -#~| msgid "%(value)s is not a valid username" -#~ msgid "“%(value)s” is not a valid UUID." -#~ msgstr "%(value)s ist kein gültiger Username" - -#, fuzzy -#~| msgid "Images" -#~ msgid "Image" -#~ msgstr "Bilder" - -#, fuzzy -#~| msgid "Relationships" -#~ msgid "One-to-one relationship" -#~ msgstr "Beziehungen" - -#, fuzzy -#~| msgid "This shelf is empty." -#~ msgid "This field is required." -#~ msgstr "Dieses Regal ist leer." - -#, fuzzy -#~| msgid "This shelf is empty." -#~ msgid "The submitted file is empty." -#~ msgstr "Dieses Regal ist leer." - -#, fuzzy -#~| msgid "%(value)s is not a valid username" -#~ msgid "“%(pk)s” is not a valid value." -#~ msgstr "%(value)s ist kein gültiger Username" - -#, fuzzy -#~| msgid "Start reading" -#~ msgid "Currently" -#~ msgstr "Gerade lesend" - -#, fuzzy -#~| msgid "Change shelf" -#~ msgid "Change" -#~ msgstr "Regal wechseln" - -#, fuzzy -#~| msgid "Started" -#~ msgid "Sat" -#~ msgstr "Gestartet" - -#, fuzzy -#~| msgid "Search" -#~ msgid "March" -#~ msgstr "Suche" - -#, fuzzy -#~| msgid "Series number:" -#~ msgid "September" -#~ msgstr "Seriennummer:" - -#, fuzzy -#~| msgid "Search" -#~ msgctxt "abbrev. month" -#~ msgid "March" -#~ msgstr "Suche" - -#, fuzzy -#~| msgid "Search" -#~ msgctxt "alt. month" -#~ msgid "March" -#~ msgstr "Suche" - -#, fuzzy -#~| msgid "Series number:" -#~ msgctxt "alt. month" -#~ msgid "September" -#~ msgstr "Seriennummer:" - -#, fuzzy -#~| msgid "No books found matching the query \"%(query)s\"" -#~ msgid "No %(verbose_name)s found matching the query" -#~ msgstr "Keine passenden Bücher zu \"%(query)s\" gefunden" - #~ msgid "Announcements" #~ msgstr "Ankündigungen" diff --git a/locale/en_US/LC_MESSAGES/django.po b/locale/en_US/LC_MESSAGES/django.po index a8720f17..6685605a 100644 --- a/locale/en_US/LC_MESSAGES/django.po +++ b/locale/en_US/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-04-01 13:14-0700\n" +"POT-Creation-Date: 2021-04-26 09:56-0700\n" "PO-Revision-Date: 2021-02-28 17:19-0800\n" "Last-Translator: Mouse Reeve \n" "Language-Team: English \n" @@ -18,35 +18,60 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: bookwyrm/forms.py:226 +#: bookwyrm/forms.py:224 msgid "A user with this email already exists." msgstr "" -#: bookwyrm/forms.py:240 +#: bookwyrm/forms.py:238 msgid "One Day" msgstr "" -#: bookwyrm/forms.py:241 +#: bookwyrm/forms.py:239 msgid "One Week" msgstr "" -#: bookwyrm/forms.py:242 +#: bookwyrm/forms.py:240 msgid "One Month" msgstr "" -#: bookwyrm/forms.py:243 +#: bookwyrm/forms.py:241 msgid "Does Not Expire" msgstr "" -#: bookwyrm/forms.py:248 +#: bookwyrm/forms.py:246 #, python-format msgid "%(count)d uses" msgstr "" -#: bookwyrm/forms.py:251 +#: bookwyrm/forms.py:249 msgid "Unlimited" msgstr "" +#: bookwyrm/forms.py:293 +msgid "List Order" +msgstr "" + +#: bookwyrm/forms.py:294 +msgid "Book Title" +msgstr "" + +#: bookwyrm/forms.py:295 bookwyrm/templates/snippets/create_status_form.html:31 +#: bookwyrm/templates/user/shelf.html:80 bookwyrm/templates/user/shelf.html:110 +msgid "Rating" +msgstr "" + +#: bookwyrm/forms.py:297 bookwyrm/templates/lists/list.html:82 +msgid "Sort By" +msgstr "" + +#: bookwyrm/forms.py:301 +msgid "Ascending" +msgstr "" + +#: bookwyrm/forms.py:302 +msgid "Descending" +msgstr "" + #: bookwyrm/models/fields.py:24 #, python-format msgid "%(value)s is not a valid remote_id" @@ -57,7 +82,7 @@ msgstr "" msgid "%(value)s is not a valid username" msgstr "" -#: bookwyrm/models/fields.py:165 bookwyrm/templates/layout.html:157 +#: bookwyrm/models/fields.py:165 bookwyrm/templates/layout.html:153 msgid "username" msgstr "" @@ -65,23 +90,23 @@ msgstr "" msgid "A user with that username already exists." msgstr "" -#: bookwyrm/settings.py:150 +#: bookwyrm/settings.py:152 msgid "English" msgstr "" -#: bookwyrm/settings.py:151 +#: bookwyrm/settings.py:153 msgid "German" msgstr "" -#: bookwyrm/settings.py:152 +#: bookwyrm/settings.py:154 msgid "Spanish" msgstr "" -#: bookwyrm/settings.py:153 +#: bookwyrm/settings.py:155 msgid "French" msgstr "" -#: bookwyrm/settings.py:154 +#: bookwyrm/settings.py:156 msgid "Simplified Chinese" msgstr "" @@ -118,80 +143,70 @@ msgstr "" msgid "Books by %(name)s" msgstr "" -#: bookwyrm/templates/book/book.html:21 +#: bookwyrm/templates/book/book.html:33 #: bookwyrm/templates/discover/large-book.html:12 #: bookwyrm/templates/discover/small-book.html:9 msgid "by" msgstr "" -#: bookwyrm/templates/book/book.html:29 bookwyrm/templates/book/book.html:30 +#: bookwyrm/templates/book/book.html:41 bookwyrm/templates/book/book.html:42 msgid "Edit Book" msgstr "" -#: bookwyrm/templates/book/book.html:49 +#: bookwyrm/templates/book/book.html:61 #: bookwyrm/templates/book/cover_modal.html:5 msgid "Add cover" msgstr "" -#: bookwyrm/templates/book/book.html:53 +#: bookwyrm/templates/book/book.html:65 msgid "Failed to load cover" msgstr "" -#: bookwyrm/templates/book/book.html:62 -msgid "ISBN:" -msgstr "" - -#: bookwyrm/templates/book/book.html:69 -#: bookwyrm/templates/book/edit_book.html:211 -msgid "OCLC Number:" -msgstr "" - -#: bookwyrm/templates/book/book.html:76 -#: bookwyrm/templates/book/edit_book.html:215 -msgid "ASIN:" -msgstr "" - -#: bookwyrm/templates/book/book.html:85 +#: bookwyrm/templates/book/book.html:82 msgid "View on OpenLibrary" msgstr "" -#: bookwyrm/templates/book/book.html:94 +#: bookwyrm/templates/book/book.html:102 #, python-format msgid "(%(review_count)s review)" msgid_plural "(%(review_count)s reviews)" msgstr[0] "" msgstr[1] "" -#: bookwyrm/templates/book/book.html:100 +#: bookwyrm/templates/book/book.html:114 msgid "Add Description" msgstr "" -#: bookwyrm/templates/book/book.html:107 -#: bookwyrm/templates/book/edit_book.html:101 +#: bookwyrm/templates/book/book.html:121 +#: bookwyrm/templates/book/edit_book.html:107 #: bookwyrm/templates/lists/form.html:12 msgid "Description:" msgstr "" -#: bookwyrm/templates/book/book.html:111 -#: bookwyrm/templates/book/edit_book.html:225 +#: bookwyrm/templates/book/book.html:125 +#: bookwyrm/templates/book/edit_book.html:240 #: bookwyrm/templates/edit_author.html:78 bookwyrm/templates/lists/form.html:42 #: bookwyrm/templates/preferences/edit_user.html:70 +#: bookwyrm/templates/settings/edit_server.html:68 +#: bookwyrm/templates/settings/federated_server.html:93 #: bookwyrm/templates/settings/site.html:93 -#: bookwyrm/templates/snippets/readthrough.html:65 +#: bookwyrm/templates/snippets/readthrough.html:75 #: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:42 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:42 #: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:34 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:38 msgid "Save" msgstr "" -#: bookwyrm/templates/book/book.html:112 bookwyrm/templates/book/book.html:161 +#: bookwyrm/templates/book/book.html:126 bookwyrm/templates/book/book.html:175 #: bookwyrm/templates/book/cover_modal.html:32 -#: bookwyrm/templates/book/edit_book.html:226 +#: bookwyrm/templates/book/edit_book.html:241 #: bookwyrm/templates/edit_author.html:79 -#: bookwyrm/templates/moderation/report_modal.html:32 +#: bookwyrm/templates/moderation/report_modal.html:34 +#: bookwyrm/templates/settings/federated_server.html:94 #: bookwyrm/templates/snippets/delete_readthrough_modal.html:17 #: bookwyrm/templates/snippets/goal_form.html:32 -#: bookwyrm/templates/snippets/readthrough.html:66 +#: bookwyrm/templates/snippets/readthrough.html:76 #: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:43 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:43 #: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:35 @@ -199,73 +214,105 @@ msgstr "" msgid "Cancel" msgstr "" -#: bookwyrm/templates/book/book.html:121 +#: bookwyrm/templates/book/book.html:135 #, python-format msgid "%(count)s editions" msgstr "" -#: bookwyrm/templates/book/book.html:129 +#: bookwyrm/templates/book/book.html:143 #, python-format msgid "This edition is on your %(shelf_name)s shelf." msgstr "" -#: bookwyrm/templates/book/book.html:135 +#: bookwyrm/templates/book/book.html:149 #, python-format -msgid "A different edition of this book is on your %(shelf_name)s shelf." -msgstr "" - -#: bookwyrm/templates/book/book.html:144 -msgid "Your reading activity" -msgstr "" - -#: bookwyrm/templates/book/book.html:146 -msgid "Add read dates" -msgstr "" - -#: bookwyrm/templates/book/book.html:151 -msgid "You don't have any reading activity for this book." +msgid "" +"A different edition of this book is on your %(shelf_name)s shelf." msgstr "" #: bookwyrm/templates/book/book.html:158 +msgid "Your reading activity" +msgstr "" + +#: bookwyrm/templates/book/book.html:160 +msgid "Add read dates" +msgstr "" + +#: bookwyrm/templates/book/book.html:165 +msgid "You don't have any reading activity for this book." +msgstr "" + +#: bookwyrm/templates/book/book.html:172 msgid "Create" msgstr "" -#: bookwyrm/templates/book/book.html:180 +#: bookwyrm/templates/book/book.html:194 msgid "Subjects" msgstr "" -#: bookwyrm/templates/book/book.html:191 +#: bookwyrm/templates/book/book.html:206 msgid "Places" msgstr "" -#: bookwyrm/templates/book/book.html:202 bookwyrm/templates/layout.html:64 +#: bookwyrm/templates/book/book.html:217 bookwyrm/templates/layout.html:65 #: bookwyrm/templates/lists/lists.html:5 bookwyrm/templates/lists/lists.html:12 #: bookwyrm/templates/search_results.html:91 #: bookwyrm/templates/user/user_layout.html:62 msgid "Lists" msgstr "" -#: bookwyrm/templates/book/book.html:213 +#: bookwyrm/templates/book/book.html:228 msgid "Add to list" msgstr "" -#: bookwyrm/templates/book/book.html:223 +#: bookwyrm/templates/book/book.html:238 #: bookwyrm/templates/book/cover_modal.html:31 -#: bookwyrm/templates/lists/list.html:90 +#: bookwyrm/templates/lists/list.html:133 msgid "Add" msgstr "" -#: bookwyrm/templates/book/book.html:251 +#: bookwyrm/templates/book/book.html:254 +msgid "Reviews" +msgstr "" + +#: bookwyrm/templates/book/book.html:259 +msgid "Your reviews" +msgstr "" + +#: bookwyrm/templates/book/book.html:265 +msgid "Your comments" +msgstr "" + +#: bookwyrm/templates/book/book.html:271 +msgid "Your quotes" +msgstr "" + +#: bookwyrm/templates/book/book.html:305 msgid "rated it" msgstr "" +#: bookwyrm/templates/book/book_identifiers.html:8 +msgid "ISBN:" +msgstr "" + +#: bookwyrm/templates/book/book_identifiers.html:15 +#: bookwyrm/templates/book/edit_book.html:226 +msgid "OCLC Number:" +msgstr "" + +#: bookwyrm/templates/book/book_identifiers.html:22 +#: bookwyrm/templates/book/edit_book.html:230 +msgid "ASIN:" +msgstr "" + #: bookwyrm/templates/book/cover_modal.html:17 -#: bookwyrm/templates/book/edit_book.html:163 +#: bookwyrm/templates/book/edit_book.html:178 msgid "Upload cover:" msgstr "" #: bookwyrm/templates/book/cover_modal.html:23 -#: bookwyrm/templates/book/edit_book.html:169 +#: bookwyrm/templates/book/edit_book.html:184 msgid "Load cover from url:" msgstr "" @@ -341,86 +388,86 @@ msgstr "" msgid "Metadata" msgstr "" -#: bookwyrm/templates/book/edit_book.html:91 +#: bookwyrm/templates/book/edit_book.html:92 msgid "Title:" msgstr "" -#: bookwyrm/templates/book/edit_book.html:96 +#: bookwyrm/templates/book/edit_book.html:100 msgid "Subtitle:" msgstr "" -#: bookwyrm/templates/book/edit_book.html:106 +#: bookwyrm/templates/book/edit_book.html:113 msgid "Series:" msgstr "" -#: bookwyrm/templates/book/edit_book.html:111 +#: bookwyrm/templates/book/edit_book.html:120 msgid "Series number:" msgstr "" -#: bookwyrm/templates/book/edit_book.html:117 +#: bookwyrm/templates/book/edit_book.html:126 msgid "Publisher:" msgstr "" -#: bookwyrm/templates/book/edit_book.html:119 +#: bookwyrm/templates/book/edit_book.html:128 msgid "Separate multiple publishers with commas." msgstr "" -#: bookwyrm/templates/book/edit_book.html:125 +#: bookwyrm/templates/book/edit_book.html:135 msgid "First published date:" msgstr "" -#: bookwyrm/templates/book/edit_book.html:130 +#: bookwyrm/templates/book/edit_book.html:143 msgid "Published date:" msgstr "" -#: bookwyrm/templates/book/edit_book.html:137 +#: bookwyrm/templates/book/edit_book.html:152 msgid "Authors" msgstr "" -#: bookwyrm/templates/book/edit_book.html:143 +#: bookwyrm/templates/book/edit_book.html:158 #, python-format msgid "Remove %(name)s" msgstr "" -#: bookwyrm/templates/book/edit_book.html:148 +#: bookwyrm/templates/book/edit_book.html:163 msgid "Add Authors:" msgstr "" -#: bookwyrm/templates/book/edit_book.html:149 +#: bookwyrm/templates/book/edit_book.html:164 msgid "John Doe, Jane Smith" msgstr "" -#: bookwyrm/templates/book/edit_book.html:155 -#: bookwyrm/templates/user/shelf.html:75 +#: bookwyrm/templates/book/edit_book.html:170 +#: bookwyrm/templates/user/shelf.html:74 msgid "Cover" msgstr "" -#: bookwyrm/templates/book/edit_book.html:182 +#: bookwyrm/templates/book/edit_book.html:197 msgid "Physical Properties" msgstr "" -#: bookwyrm/templates/book/edit_book.html:183 +#: bookwyrm/templates/book/edit_book.html:198 #: bookwyrm/templates/book/format_filter.html:5 msgid "Format:" msgstr "" -#: bookwyrm/templates/book/edit_book.html:191 +#: bookwyrm/templates/book/edit_book.html:206 msgid "Pages:" msgstr "" -#: bookwyrm/templates/book/edit_book.html:198 +#: bookwyrm/templates/book/edit_book.html:213 msgid "Book Identifiers" msgstr "" -#: bookwyrm/templates/book/edit_book.html:199 +#: bookwyrm/templates/book/edit_book.html:214 msgid "ISBN 13:" msgstr "" -#: bookwyrm/templates/book/edit_book.html:203 +#: bookwyrm/templates/book/edit_book.html:218 msgid "ISBN 10:" msgstr "" -#: bookwyrm/templates/book/edit_book.html:207 +#: bookwyrm/templates/book/edit_book.html:222 #: bookwyrm/templates/edit_author.html:59 msgid "Openlibrary key:" msgstr "" @@ -444,44 +491,53 @@ msgstr "" msgid "Language:" msgstr "" -#: bookwyrm/templates/book/publisher_info.html:6 +#: bookwyrm/templates/book/publisher_info.html:22 +#, python-format +msgid "%(format)s" +msgstr "" + +#: bookwyrm/templates/book/publisher_info.html:24 #, python-format msgid "%(format)s, %(pages)s pages" msgstr "" -#: bookwyrm/templates/book/publisher_info.html:8 +#: bookwyrm/templates/book/publisher_info.html:26 #, python-format msgid "%(pages)s pages" msgstr "" -#: bookwyrm/templates/book/publisher_info.html:13 +#: bookwyrm/templates/book/publisher_info.html:38 #, python-format msgid "%(languages)s language" msgstr "" -#: bookwyrm/templates/book/publisher_info.html:18 +#: bookwyrm/templates/book/publisher_info.html:64 #, python-format msgid "Published %(date)s by %(publisher)s." msgstr "" -#: bookwyrm/templates/book/publisher_info.html:20 +#: bookwyrm/templates/book/publisher_info.html:66 #, python-format msgid "Published %(date)s" msgstr "" -#: bookwyrm/templates/book/publisher_info.html:22 +#: bookwyrm/templates/book/publisher_info.html:68 #, python-format msgid "Published by %(publisher)s." msgstr "" #: bookwyrm/templates/components/inline_form.html:8 #: bookwyrm/templates/components/modal.html:11 -#: bookwyrm/templates/feed/feed_layout.html:57 +#: bookwyrm/templates/feed/feed_layout.html:70 #: bookwyrm/templates/get_started/layout.html:19 #: bookwyrm/templates/get_started/layout.html:52 msgid "Close" msgstr "" +#: bookwyrm/templates/compose.html:5 bookwyrm/templates/compose.html:8 +msgid "Compose status" +msgstr "" + #: bookwyrm/templates/directory/community_filter.html:5 msgid "Community" msgstr "" @@ -496,7 +552,7 @@ msgstr "" #: bookwyrm/templates/directory/directory.html:6 #: bookwyrm/templates/directory/directory.html:11 -#: bookwyrm/templates/layout.html:97 +#: bookwyrm/templates/layout.html:93 msgid "Directory" msgstr "" @@ -506,7 +562,9 @@ msgstr "" #: bookwyrm/templates/directory/directory.html:26 #, python-format -msgid "You can opt-out at any time in your profile settings." +msgid "" +"You can opt-out at any time in your profile settings." msgstr "" #: bookwyrm/templates/directory/directory.html:31 @@ -620,7 +678,7 @@ msgid "Email address:" msgstr "" #: bookwyrm/templates/discover/landing_layout.html:70 -#: bookwyrm/templates/moderation/report_modal.html:31 +#: bookwyrm/templates/moderation/report_modal.html:33 msgid "Submit" msgstr "" @@ -673,7 +731,9 @@ msgstr "" #: bookwyrm/templates/email/html_layout.html:21 #, python-format -msgid "BookWyrm hosted on %(site_name)s" +msgid "" +"BookWyrm hosted on " +"%(site_name)s" msgstr "" #: bookwyrm/templates/email/html_layout.html:23 @@ -692,12 +752,16 @@ msgstr "" #: bookwyrm/templates/email/invite/html_content.html:15 #, python-format -msgid "Learn more about this instance." +msgid "" +"Learn more about this instance." msgstr "" #: bookwyrm/templates/email/invite/text_content.html:4 #, python-format -msgid "You're invited to join %(site_name)s! Click the link below to create an account." +msgid "" +"You're invited to join %(site_name)s! Click the link below to create an " +"account." msgstr "" #: bookwyrm/templates/email/invite/text_content.html:8 @@ -707,7 +771,9 @@ msgstr "" #: bookwyrm/templates/email/password_reset/html_content.html:6 #: bookwyrm/templates/email/password_reset/text_content.html:4 #, python-format -msgid "You requested to reset your %(site_name)s password. Click the link below to set a new password and log in to your account." +msgid "" +"You requested to reset your %(site_name)s password. Click the link below to " +"set a new password and log in to your account." msgstr "" #: bookwyrm/templates/email/password_reset/html_content.html:9 @@ -720,7 +786,8 @@ msgstr "" #: bookwyrm/templates/email/password_reset/html_content.html:13 #: bookwyrm/templates/email/password_reset/text_content.html:8 -msgid "If you didn't request to reset your password, you can ignore this email." +msgid "" +"If you didn't request to reset your password, you can ignore this email." msgstr "" #: bookwyrm/templates/email/password_reset/subject.html:2 @@ -734,7 +801,7 @@ msgid "Direct Messages with %(username)s" msgstr "" #: bookwyrm/templates/feed/direct_messages.html:10 -#: bookwyrm/templates/layout.html:87 +#: bookwyrm/templates/layout.html:88 msgid "Direct Messages" msgstr "" @@ -767,6 +834,7 @@ msgid "Local" msgstr "" #: bookwyrm/templates/feed/feed.html:25 +#: bookwyrm/templates/settings/edit_server.html:40 msgid "Federated" msgstr "" @@ -776,7 +844,8 @@ msgid "load 0 unread status(es)" msgstr "" #: bookwyrm/templates/feed/feed.html:48 -msgid "There aren't any activities right now! Try following a user to get started" +msgid "" +"There aren't any activities right now! Try following a user to get started" msgstr "" #: bookwyrm/templates/feed/feed.html:56 @@ -789,32 +858,33 @@ msgid "Updates" msgstr "" #: bookwyrm/templates/feed/feed_layout.html:11 -#: bookwyrm/templates/layout.html:58 +#: bookwyrm/templates/layout.html:59 #: bookwyrm/templates/user/books_header.html:3 msgid "Your books" msgstr "" #: bookwyrm/templates/feed/feed_layout.html:13 -msgid "There are no books here right now! Try searching for a book to get started" -msgstr "" - -#: bookwyrm/templates/feed/feed_layout.html:23 -#: bookwyrm/templates/user/shelf.html:28 -msgid "To Read" +msgid "" +"There are no books here right now! Try searching for a book to get started" msgstr "" #: bookwyrm/templates/feed/feed_layout.html:24 #: bookwyrm/templates/user/shelf.html:28 -msgid "Currently Reading" +msgid "To Read" msgstr "" #: bookwyrm/templates/feed/feed_layout.html:25 +#: bookwyrm/templates/user/shelf.html:28 +msgid "Currently Reading" +msgstr "" + +#: bookwyrm/templates/feed/feed_layout.html:26 #: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:11 #: bookwyrm/templates/user/shelf.html:28 msgid "Read" msgstr "" -#: bookwyrm/templates/feed/feed_layout.html:74 bookwyrm/templates/goal.html:26 +#: bookwyrm/templates/feed/feed_layout.html:88 bookwyrm/templates/goal.html:26 #: bookwyrm/templates/snippets/goal_card.html:6 #, python-format msgid "%(year)s Reading Goal" @@ -844,7 +914,7 @@ msgid "What are you reading?" msgstr "" #: bookwyrm/templates/get_started/books.html:9 -#: bookwyrm/templates/lists/list.html:58 +#: bookwyrm/templates/lists/list.html:101 msgid "Search for a book" msgstr "" @@ -864,8 +934,8 @@ msgstr "" #: bookwyrm/templates/get_started/books.html:17 #: bookwyrm/templates/get_started/users.html:18 #: bookwyrm/templates/get_started/users.html:19 -#: bookwyrm/templates/layout.html:37 bookwyrm/templates/layout.html:38 -#: bookwyrm/templates/lists/list.html:62 +#: bookwyrm/templates/layout.html:38 bookwyrm/templates/layout.html:39 +#: bookwyrm/templates/lists/list.html:105 msgid "Search" msgstr "" @@ -879,7 +949,7 @@ msgid "Popular on %(site_name)s" msgstr "" #: bookwyrm/templates/get_started/books.html:51 -#: bookwyrm/templates/lists/list.html:75 +#: bookwyrm/templates/lists/list.html:118 msgid "No books found" msgstr "" @@ -948,7 +1018,9 @@ msgid "Show this account in suggested users:" msgstr "" #: bookwyrm/templates/get_started/profile.html:52 -msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users." +msgid "" +"Your account will show up in the directory, and may be recommended to other " +"BookWyrm users." msgstr "" #: bookwyrm/templates/get_started/users.html:11 @@ -973,7 +1045,9 @@ msgstr "" #: bookwyrm/templates/goal.html:30 #: bookwyrm/templates/snippets/goal_card.html:13 #, python-format -msgid "Set a goal for how many books you'll finish reading in %(year)s, and track your progress throughout the year." +msgid "" +"Set a goal for how many books you'll finish reading in %(year)s, and track " +"your progress throughout the year." msgstr "" #: bookwyrm/templates/goal.html:39 @@ -992,7 +1066,7 @@ msgid "%(username)s's %(year)s Books" msgstr "" #: bookwyrm/templates/import.html:5 bookwyrm/templates/import.html:9 -#: bookwyrm/templates/layout.html:102 +#: bookwyrm/templates/layout.html:98 msgid "Import Books" msgstr "" @@ -1013,6 +1087,7 @@ msgid "Privacy setting for imported reviews:" msgstr "" #: bookwyrm/templates/import.html:48 +#: bookwyrm/templates/settings/server_blocklist.html:64 msgid "Import" msgstr "" @@ -1055,7 +1130,9 @@ msgstr "" #: bookwyrm/templates/import_status.html:44 #, python-format -msgid "Jump to the bottom of the list to select the %(failed_count)s items which failed to import." +msgid "" +"Jump to the bottom of the list to select the %(failed_count)s items which " +"failed to import." msgstr "" #: bookwyrm/templates/import_status.html:79 @@ -1077,12 +1154,12 @@ msgstr "" #: bookwyrm/templates/import_status.html:115 #: bookwyrm/templates/snippets/create_status_form.html:10 -#: bookwyrm/templates/user/shelf.html:76 +#: bookwyrm/templates/user/shelf.html:75 bookwyrm/templates/user/shelf.html:93 msgid "Title" msgstr "" #: bookwyrm/templates/import_status.html:118 -#: bookwyrm/templates/user/shelf.html:77 +#: bookwyrm/templates/user/shelf.html:76 bookwyrm/templates/user/shelf.html:96 msgid "Author" msgstr "" @@ -1119,91 +1196,90 @@ msgstr "" msgid "Matching Books" msgstr "" -#: bookwyrm/templates/layout.html:33 +#: bookwyrm/templates/layout.html:34 msgid "Search for a book or user" msgstr "" -#: bookwyrm/templates/layout.html:47 bookwyrm/templates/layout.html:48 +#: bookwyrm/templates/layout.html:48 bookwyrm/templates/layout.html:49 msgid "Main navigation menu" msgstr "" -#: bookwyrm/templates/layout.html:61 +#: bookwyrm/templates/layout.html:62 msgid "Feed" msgstr "" -#: bookwyrm/templates/layout.html:92 -#: bookwyrm/templates/preferences/preferences_layout.html:14 -msgid "Profile" -msgstr "" - -#: bookwyrm/templates/layout.html:107 +#: bookwyrm/templates/layout.html:103 msgid "Settings" msgstr "" -#: bookwyrm/templates/layout.html:116 -#: bookwyrm/templates/settings/admin_layout.html:24 +#: bookwyrm/templates/layout.html:112 +#: bookwyrm/templates/settings/admin_layout.html:31 #: bookwyrm/templates/settings/manage_invite_requests.html:15 #: bookwyrm/templates/settings/manage_invites.html:3 #: bookwyrm/templates/settings/manage_invites.html:15 msgid "Invites" msgstr "" -#: bookwyrm/templates/layout.html:123 +#: bookwyrm/templates/layout.html:119 msgid "Admin" msgstr "" -#: bookwyrm/templates/layout.html:130 +#: bookwyrm/templates/layout.html:126 msgid "Log out" msgstr "" -#: bookwyrm/templates/layout.html:138 bookwyrm/templates/layout.html:139 +#: bookwyrm/templates/layout.html:134 bookwyrm/templates/layout.html:135 #: bookwyrm/templates/notifications.html:6 #: bookwyrm/templates/notifications.html:10 msgid "Notifications" msgstr "" -#: bookwyrm/templates/layout.html:156 bookwyrm/templates/layout.html:160 +#: bookwyrm/templates/layout.html:152 bookwyrm/templates/layout.html:156 #: bookwyrm/templates/login.html:17 #: bookwyrm/templates/snippets/register_form.html:4 msgid "Username:" msgstr "" -#: bookwyrm/templates/layout.html:161 +#: bookwyrm/templates/layout.html:157 msgid "password" msgstr "" -#: bookwyrm/templates/layout.html:162 bookwyrm/templates/login.html:36 +#: bookwyrm/templates/layout.html:158 bookwyrm/templates/login.html:36 msgid "Forgot your password?" msgstr "" -#: bookwyrm/templates/layout.html:165 bookwyrm/templates/login.html:10 +#: bookwyrm/templates/layout.html:161 bookwyrm/templates/login.html:10 #: bookwyrm/templates/login.html:33 msgid "Log in" msgstr "" -#: bookwyrm/templates/layout.html:173 +#: bookwyrm/templates/layout.html:169 msgid "Join" msgstr "" -#: bookwyrm/templates/layout.html:196 +#: bookwyrm/templates/layout.html:195 msgid "About this server" msgstr "" -#: bookwyrm/templates/layout.html:200 +#: bookwyrm/templates/layout.html:199 msgid "Contact site admin" msgstr "" -#: bookwyrm/templates/layout.html:207 +#: bookwyrm/templates/layout.html:206 #, python-format -msgid "Support %(site_name)s on %(support_title)s" +msgid "" +"Support %(site_name)s on " +"%(support_title)s" msgstr "" -#: bookwyrm/templates/layout.html:211 -msgid "BookWyrm is open source software. You can contribute or report issues on GitHub." +#: bookwyrm/templates/layout.html:210 +msgid "" +"BookWyrm is open source software. You can contribute or report issues on GitHub." msgstr "" #: bookwyrm/templates/lists/create_form.html:5 -#: bookwyrm/templates/lists/lists.html:19 +#: bookwyrm/templates/lists/lists.html:20 msgid "Create List" msgstr "" @@ -1267,7 +1343,7 @@ msgid "Anyone can suggest books, subject to your approval" msgstr "" #: bookwyrm/templates/lists/form.html:31 -#: bookwyrm/templates/moderation/reports.html:24 +#: bookwyrm/templates/moderation/reports.html:25 msgid "Open" msgstr "" @@ -1275,45 +1351,73 @@ msgstr "" msgid "Anyone can add books to this list" msgstr "" -#: bookwyrm/templates/lists/list.html:17 +#: bookwyrm/templates/lists/list.html:19 +msgid "You successfully suggested a book for this list!" +msgstr "" + +#: bookwyrm/templates/lists/list.html:21 +msgid "You successfully added a book to this list!" +msgstr "" + +#: bookwyrm/templates/lists/list.html:27 msgid "This list is currently empty" msgstr "" -#: bookwyrm/templates/lists/list.html:35 +#: bookwyrm/templates/lists/list.html:46 #, python-format msgid "Added by %(username)s" msgstr "" -#: bookwyrm/templates/lists/list.html:41 -#: bookwyrm/templates/snippets/shelf_selector.html:28 +#: bookwyrm/templates/lists/list.html:58 +msgid "Set" +msgstr "" + +#: bookwyrm/templates/lists/list.html:61 +msgid "List position" +msgstr "" + +#: bookwyrm/templates/lists/list.html:67 +#: bookwyrm/templates/snippets/shelf_selector.html:26 msgid "Remove" msgstr "" -#: bookwyrm/templates/lists/list.html:54 +#: bookwyrm/templates/lists/list.html:80 bookwyrm/templates/lists/list.html:92 +msgid "Sort List" +msgstr "" + +#: bookwyrm/templates/lists/list.html:86 +msgid "Direction" +msgstr "" + +#: bookwyrm/templates/lists/list.html:97 msgid "Add Books" msgstr "" -#: bookwyrm/templates/lists/list.html:54 +#: bookwyrm/templates/lists/list.html:97 msgid "Suggest Books" msgstr "" -#: bookwyrm/templates/lists/list.html:63 +#: bookwyrm/templates/lists/list.html:106 msgid "search" msgstr "" -#: bookwyrm/templates/lists/list.html:69 +#: bookwyrm/templates/lists/list.html:112 msgid "Clear search" msgstr "" -#: bookwyrm/templates/lists/list.html:74 +#: bookwyrm/templates/lists/list.html:117 #, python-format msgid "No books found matching the query \"%(query)s\"" msgstr "" -#: bookwyrm/templates/lists/list.html:90 +#: bookwyrm/templates/lists/list.html:133 msgid "Suggest" msgstr "" +#: bookwyrm/templates/lists/lists.html:14 bookwyrm/templates/user/lists.html:9 +msgid "Your Lists" +msgstr "" + #: bookwyrm/templates/login.html:4 msgid "Login" msgstr "" @@ -1331,59 +1435,37 @@ msgstr "" msgid "More about this site" msgstr "" -#: bookwyrm/templates/moderation/report.html:5 #: bookwyrm/templates/moderation/report.html:6 +#: bookwyrm/templates/moderation/report.html:7 #: bookwyrm/templates/moderation/report_preview.html:6 #, python-format msgid "Report #%(report_id)s: %(username)s" msgstr "" -#: bookwyrm/templates/moderation/report.html:10 +#: bookwyrm/templates/moderation/report.html:11 msgid "Back to reports" msgstr "" -#: bookwyrm/templates/moderation/report.html:18 -msgid "Actions" -msgstr "" - -#: bookwyrm/templates/moderation/report.html:19 -msgid "View user profile" -msgstr "" - -#: bookwyrm/templates/moderation/report.html:22 -#: bookwyrm/templates/snippets/status/status_options.html:25 -#: bookwyrm/templates/snippets/user_options.html:13 -msgid "Send direct message" -msgstr "" - -#: bookwyrm/templates/moderation/report.html:27 -msgid "Deactivate user" -msgstr "" - -#: bookwyrm/templates/moderation/report.html:29 -msgid "Reactivate user" -msgstr "" - -#: bookwyrm/templates/moderation/report.html:36 +#: bookwyrm/templates/moderation/report.html:23 msgid "Moderator Comments" msgstr "" -#: bookwyrm/templates/moderation/report.html:54 -#: bookwyrm/templates/snippets/create_status.html:12 -#: bookwyrm/templates/snippets/create_status_form.html:52 +#: bookwyrm/templates/moderation/report.html:41 +#: bookwyrm/templates/snippets/create_status.html:28 +#: bookwyrm/templates/snippets/create_status_form.html:53 msgid "Comment" msgstr "" -#: bookwyrm/templates/moderation/report.html:59 +#: bookwyrm/templates/moderation/report.html:46 msgid "Reported statuses" msgstr "" -#: bookwyrm/templates/moderation/report.html:61 +#: bookwyrm/templates/moderation/report.html:48 msgid "No statuses reported" msgstr "" -#: bookwyrm/templates/moderation/report.html:67 -msgid "Statuses has been deleted" +#: bookwyrm/templates/moderation/report.html:54 +msgid "Status has been deleted" msgstr "" #: bookwyrm/templates/moderation/report_modal.html:6 @@ -1391,12 +1473,12 @@ msgstr "" msgid "Report @%(username)s" msgstr "" -#: bookwyrm/templates/moderation/report_modal.html:21 +#: bookwyrm/templates/moderation/report_modal.html:23 #, python-format msgid "This report will be sent to %(site_name)s's moderators for review." msgstr "" -#: bookwyrm/templates/moderation/report_modal.html:22 +#: bookwyrm/templates/moderation/report_modal.html:24 msgid "More info about this report:" msgstr "" @@ -1423,21 +1505,21 @@ msgid "Reports: %(server_name)s" msgstr "" #: bookwyrm/templates/moderation/reports.html:8 -#: bookwyrm/templates/moderation/reports.html:16 -#: bookwyrm/templates/settings/admin_layout.html:28 +#: bookwyrm/templates/moderation/reports.html:17 +#: bookwyrm/templates/settings/admin_layout.html:35 msgid "Reports" msgstr "" -#: bookwyrm/templates/moderation/reports.html:13 +#: bookwyrm/templates/moderation/reports.html:14 #, python-format msgid "Reports: %(server_name)s" msgstr "" -#: bookwyrm/templates/moderation/reports.html:27 +#: bookwyrm/templates/moderation/reports.html:28 msgid "Resolved" msgstr "" -#: bookwyrm/templates/moderation/reports.html:34 +#: bookwyrm/templates/moderation/reports.html:37 msgid "No reports found." msgstr "" @@ -1447,17 +1529,23 @@ msgstr "" #: bookwyrm/templates/notifications.html:53 #, python-format -msgid "favorited your review of %(book_title)s" +msgid "" +"favorited your review of %(book_title)s" msgstr "" #: bookwyrm/templates/notifications.html:55 #, python-format -msgid "favorited your comment on %(book_title)s" +msgid "" +"favorited your comment on %(book_title)s" msgstr "" #: bookwyrm/templates/notifications.html:57 #, python-format -msgid "favorited your quote from %(book_title)s" +msgid "" +"favorited your quote from %(book_title)s" msgstr "" #: bookwyrm/templates/notifications.html:59 @@ -1467,17 +1555,23 @@ msgstr "" #: bookwyrm/templates/notifications.html:64 #, python-format -msgid "mentioned you in a review of %(book_title)s" +msgid "" +"mentioned you in a review of " +"%(book_title)s" msgstr "" #: bookwyrm/templates/notifications.html:66 #, python-format -msgid "mentioned you in a comment on %(book_title)s" +msgid "" +"mentioned you in a comment on " +"%(book_title)s" msgstr "" #: bookwyrm/templates/notifications.html:68 #, python-format -msgid "mentioned you in a quote from %(book_title)s" +msgid "" +"mentioned you in a quote from " +"%(book_title)s" msgstr "" #: bookwyrm/templates/notifications.html:70 @@ -1487,22 +1581,30 @@ msgstr "" #: bookwyrm/templates/notifications.html:75 #, python-format -msgid "replied to your review of %(book_title)s" +msgid "" +"replied to your review of %(book_title)s" msgstr "" #: bookwyrm/templates/notifications.html:77 #, python-format -msgid "replied to your comment on %(book_title)s" +msgid "" +"replied to your comment on %(book_title)s" msgstr "" #: bookwyrm/templates/notifications.html:79 #, python-format -msgid "replied to your quote from %(book_title)s" +msgid "" +"replied to your quote from %(book_title)s" msgstr "" #: bookwyrm/templates/notifications.html:81 #, python-format -msgid "replied to your status" +msgid "" +"replied to your status" msgstr "" #: bookwyrm/templates/notifications.html:85 @@ -1515,17 +1617,23 @@ msgstr "" #: bookwyrm/templates/notifications.html:94 #, python-format -msgid "boosted your review of %(book_title)s" +msgid "" +"boosted your review of %(book_title)s" msgstr "" #: bookwyrm/templates/notifications.html:96 #, python-format -msgid "boosted your comment on%(book_title)s" +msgid "" +"boosted your comment on%(book_title)s" msgstr "" #: bookwyrm/templates/notifications.html:98 #, python-format -msgid "boosted your quote from %(book_title)s" +msgid "" +"boosted your quote from %(book_title)s" msgstr "" #: bookwyrm/templates/notifications.html:100 @@ -1535,12 +1643,16 @@ msgstr "" #: bookwyrm/templates/notifications.html:104 #, python-format -msgid " added %(book_title)s to your list \"%(list_name)s\"" +msgid "" +" added %(book_title)s to your list " +"\"%(list_name)s\"" msgstr "" #: bookwyrm/templates/notifications.html:106 #, python-format -msgid " suggested adding %(book_title)s to your list \"%(list_name)s\"" +msgid "" +" suggested adding %(book_title)s to " +"your list \"%(list_name)s\"" msgstr "" #: bookwyrm/templates/notifications.html:110 @@ -1602,7 +1714,9 @@ msgstr "" #: bookwyrm/templates/preferences/edit_user.html:62 #, python-format -msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users." +msgid "" +"Your account will show up in the directory, and may " +"be recommended to other BookWyrm users." msgstr "" #: bookwyrm/templates/preferences/edit_user.html:65 @@ -1613,6 +1727,10 @@ msgstr "" msgid "Account" msgstr "" +#: bookwyrm/templates/preferences/preferences_layout.html:14 +msgid "Profile" +msgstr "" + #: bookwyrm/templates/preferences/preferences_layout.html:20 msgid "Relationships" msgstr "" @@ -1642,117 +1760,187 @@ msgstr "" msgid "Administration" msgstr "" -#: bookwyrm/templates/settings/admin_layout.html:15 +#: bookwyrm/templates/settings/admin_layout.html:22 msgid "Manage Users" msgstr "" -#: bookwyrm/templates/settings/admin_layout.html:19 -#: bookwyrm/templates/settings/user_admin.html:3 -#: bookwyrm/templates/settings/user_admin.html:10 +#: bookwyrm/templates/settings/admin_layout.html:26 +#: bookwyrm/templates/user_admin/user_admin.html:3 +#: bookwyrm/templates/user_admin/user_admin.html:10 msgid "Users" msgstr "" -#: bookwyrm/templates/settings/admin_layout.html:32 +#: bookwyrm/templates/settings/admin_layout.html:39 #: bookwyrm/templates/settings/federation.html:3 #: bookwyrm/templates/settings/federation.html:5 msgid "Federated Servers" msgstr "" -#: bookwyrm/templates/settings/admin_layout.html:37 +#: bookwyrm/templates/settings/admin_layout.html:44 msgid "Instance Settings" msgstr "" -#: bookwyrm/templates/settings/admin_layout.html:41 +#: bookwyrm/templates/settings/admin_layout.html:48 #: bookwyrm/templates/settings/site.html:4 #: bookwyrm/templates/settings/site.html:6 msgid "Site Settings" msgstr "" -#: bookwyrm/templates/settings/admin_layout.html:44 +#: bookwyrm/templates/settings/admin_layout.html:51 #: bookwyrm/templates/settings/site.html:13 msgid "Instance Info" msgstr "" -#: bookwyrm/templates/settings/admin_layout.html:45 +#: bookwyrm/templates/settings/admin_layout.html:52 #: bookwyrm/templates/settings/site.html:39 msgid "Images" msgstr "" -#: bookwyrm/templates/settings/admin_layout.html:46 +#: bookwyrm/templates/settings/admin_layout.html:53 #: bookwyrm/templates/settings/site.html:59 msgid "Footer Content" msgstr "" -#: bookwyrm/templates/settings/admin_layout.html:47 +#: bookwyrm/templates/settings/admin_layout.html:54 #: bookwyrm/templates/settings/site.html:77 msgid "Registration" msgstr "" -#: bookwyrm/templates/settings/federated_server.html:7 +#: bookwyrm/templates/settings/edit_server.html:3 +#: bookwyrm/templates/settings/edit_server.html:6 +#: bookwyrm/templates/settings/edit_server.html:20 +#: bookwyrm/templates/settings/federation.html:9 +#: bookwyrm/templates/settings/federation.html:10 +#: bookwyrm/templates/settings/server_blocklist.html:3 +#: bookwyrm/templates/settings/server_blocklist.html:20 +msgid "Add server" +msgstr "" + +#: bookwyrm/templates/settings/edit_server.html:7 +#: bookwyrm/templates/settings/federated_server.html:12 +#: bookwyrm/templates/settings/server_blocklist.html:7 msgid "Back to server list" msgstr "" -#: bookwyrm/templates/settings/federated_server.html:12 -msgid "Details" +#: bookwyrm/templates/settings/edit_server.html:16 +#: bookwyrm/templates/settings/server_blocklist.html:16 +msgid "Import block list" msgstr "" -#: bookwyrm/templates/settings/federated_server.html:15 -msgid "Software:" +#: bookwyrm/templates/settings/edit_server.html:30 +msgid "Instance:" msgstr "" -#: bookwyrm/templates/settings/federated_server.html:19 -msgid "Version:" -msgstr "" - -#: bookwyrm/templates/settings/federated_server.html:23 +#: bookwyrm/templates/settings/edit_server.html:37 +#: bookwyrm/templates/settings/federated_server.html:29 +#: bookwyrm/templates/user_admin/user_info.html:34 msgid "Status:" msgstr "" -#: bookwyrm/templates/settings/federated_server.html:30 +#: bookwyrm/templates/settings/edit_server.html:41 +#: bookwyrm/templates/settings/federated_server.html:9 +msgid "Blocked" +msgstr "" + +#: bookwyrm/templates/settings/edit_server.html:48 +#: bookwyrm/templates/settings/federated_server.html:21 +#: bookwyrm/templates/user_admin/user_info.html:26 +msgid "Software:" +msgstr "" + +#: bookwyrm/templates/settings/edit_server.html:55 +#: bookwyrm/templates/settings/federated_server.html:25 +#: bookwyrm/templates/user_admin/user_info.html:30 +msgid "Version:" +msgstr "" + +#: bookwyrm/templates/settings/edit_server.html:64 +msgid "Notes:" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:18 +msgid "Details" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:36 #: bookwyrm/templates/user/user_layout.html:50 msgid "Activity" msgstr "" -#: bookwyrm/templates/settings/federated_server.html:33 +#: bookwyrm/templates/settings/federated_server.html:39 msgid "Users:" msgstr "" -#: bookwyrm/templates/settings/federated_server.html:36 -#: bookwyrm/templates/settings/federated_server.html:43 +#: bookwyrm/templates/settings/federated_server.html:42 +#: bookwyrm/templates/settings/federated_server.html:49 msgid "View all" msgstr "" -#: bookwyrm/templates/settings/federated_server.html:40 +#: bookwyrm/templates/settings/federated_server.html:46 msgid "Reports:" msgstr "" -#: bookwyrm/templates/settings/federated_server.html:47 +#: bookwyrm/templates/settings/federated_server.html:53 msgid "Followed by us:" msgstr "" -#: bookwyrm/templates/settings/federated_server.html:53 +#: bookwyrm/templates/settings/federated_server.html:59 msgid "Followed by them:" msgstr "" -#: bookwyrm/templates/settings/federated_server.html:59 +#: bookwyrm/templates/settings/federated_server.html:65 msgid "Blocked by us:" msgstr "" -#: bookwyrm/templates/settings/federation.html:13 +#: bookwyrm/templates/settings/federated_server.html:77 +#: bookwyrm/templates/user_admin/user_info.html:39 +msgid "Notes" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:80 +msgid "Edit" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:100 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:3 +msgid "Actions" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:104 +#: bookwyrm/templates/snippets/block_button.html:5 +msgid "Block" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:105 +msgid "All users from this instance will be deactivated." +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:110 +#: bookwyrm/templates/snippets/block_button.html:10 +msgid "Un-block" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:111 +msgid "All users from this instance will be re-activated." +msgstr "" + +#: bookwyrm/templates/settings/federation.html:20 +#: bookwyrm/templates/user_admin/server_filter.html:5 msgid "Server name" msgstr "" -#: bookwyrm/templates/settings/federation.html:17 +#: bookwyrm/templates/settings/federation.html:24 msgid "Date federated" msgstr "" -#: bookwyrm/templates/settings/federation.html:21 +#: bookwyrm/templates/settings/federation.html:28 msgid "Software" msgstr "" -#: bookwyrm/templates/settings/federation.html:24 -#: bookwyrm/templates/settings/manage_invite_requests.html:33 -#: bookwyrm/templates/settings/user_admin.html:32 +#: bookwyrm/templates/settings/federation.html:31 +#: bookwyrm/templates/settings/manage_invite_requests.html:44 +#: bookwyrm/templates/settings/status_filter.html:5 +#: bookwyrm/templates/user_admin/user_admin.html:34 msgid "Status" msgstr "" @@ -1767,55 +1955,62 @@ msgstr "" msgid "Ignored Invite Requests" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:31 -msgid "Date" +#: bookwyrm/templates/settings/manage_invite_requests.html:35 +msgid "Date requested" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:32 +#: bookwyrm/templates/settings/manage_invite_requests.html:39 +msgid "Date accepted" +msgstr "" + +#: bookwyrm/templates/settings/manage_invite_requests.html:42 msgid "Email" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:34 +#: bookwyrm/templates/settings/manage_invite_requests.html:47 msgid "Action" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:37 +#: bookwyrm/templates/settings/manage_invite_requests.html:50 msgid "No requests" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:45 +#: bookwyrm/templates/settings/manage_invite_requests.html:59 +#: bookwyrm/templates/settings/status_filter.html:16 msgid "Accepted" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:47 +#: bookwyrm/templates/settings/manage_invite_requests.html:61 +#: bookwyrm/templates/settings/status_filter.html:12 msgid "Sent" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:49 +#: bookwyrm/templates/settings/manage_invite_requests.html:63 +#: bookwyrm/templates/settings/status_filter.html:8 msgid "Requested" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:57 +#: bookwyrm/templates/settings/manage_invite_requests.html:73 msgid "Send invite" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:59 +#: bookwyrm/templates/settings/manage_invite_requests.html:75 msgid "Re-send invite" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:70 +#: bookwyrm/templates/settings/manage_invite_requests.html:95 msgid "Ignore" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:72 -msgid "Un-gnore" +#: bookwyrm/templates/settings/manage_invite_requests.html:97 +msgid "Un-ignore" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:83 +#: bookwyrm/templates/settings/manage_invite_requests.html:108 msgid "Back to pending requests" msgstr "" -#: bookwyrm/templates/settings/manage_invite_requests.html:85 +#: bookwyrm/templates/settings/manage_invite_requests.html:110 msgid "View ignored requests" msgstr "" @@ -1855,6 +2050,23 @@ msgstr "" msgid "No active invites" msgstr "" +#: bookwyrm/templates/settings/server_blocklist.html:6 +msgid "Import Blocklist" +msgstr "" + +#: bookwyrm/templates/settings/server_blocklist.html:26 +#: bookwyrm/templates/snippets/goal_progress.html:5 +msgid "Success!" +msgstr "" + +#: bookwyrm/templates/settings/server_blocklist.html:30 +msgid "Successfully blocked:" +msgstr "" + +#: bookwyrm/templates/settings/server_blocklist.html:32 +msgid "Failed:" +msgstr "" + #: bookwyrm/templates/settings/site.html:15 msgid "Instance Name:" msgstr "" @@ -1911,138 +2123,96 @@ msgstr "" msgid "Registration closed text:" msgstr "" -#: bookwyrm/templates/settings/user_admin.html:7 -#, python-format -msgid "Users: %(server_name)s" +#: bookwyrm/templates/snippets/book_cover.html:20 +#: bookwyrm/templates/snippets/search_result_text.html:10 +msgid "No cover" msgstr "" -#: bookwyrm/templates/settings/user_admin.html:20 -msgid "Username" -msgstr "" - -#: bookwyrm/templates/settings/user_admin.html:24 -msgid "Date Added" -msgstr "" - -#: bookwyrm/templates/settings/user_admin.html:28 -msgid "Last Active" -msgstr "" - -#: bookwyrm/templates/settings/user_admin.html:36 -msgid "Remote server" -msgstr "" - -#: bookwyrm/templates/settings/user_admin.html:45 -msgid "Active" -msgstr "" - -#: bookwyrm/templates/settings/user_admin.html:45 -msgid "Inactive" -msgstr "" - -#: bookwyrm/templates/settings/user_admin.html:50 -msgid "Not set" -msgstr "" - -#: bookwyrm/templates/snippets/block_button.html:5 -msgid "Block" -msgstr "" - -#: bookwyrm/templates/snippets/block_button.html:10 -msgid "Un-block" -msgstr "" - -#: bookwyrm/templates/snippets/book_titleby.html:3 +#: bookwyrm/templates/snippets/book_titleby.html:4 #, python-format msgid "%(title)s by " msgstr "" #: bookwyrm/templates/snippets/boost_button.html:8 #: bookwyrm/templates/snippets/boost_button.html:9 -#: bookwyrm/templates/snippets/status/status_body.html:51 -#: bookwyrm/templates/snippets/status/status_body.html:52 -msgid "Boost status" +msgid "Boost" msgstr "" +#: bookwyrm/templates/snippets/boost_button.html:15 #: bookwyrm/templates/snippets/boost_button.html:16 -#: bookwyrm/templates/snippets/boost_button.html:17 -msgid "Un-boost status" +msgid "Un-boost" msgstr "" #: bookwyrm/templates/snippets/content_warning_field.html:3 msgid "Spoiler alert:" msgstr "" -#: bookwyrm/templates/snippets/content_warning_field.html:4 +#: bookwyrm/templates/snippets/content_warning_field.html:10 msgid "Spoilers ahead!" msgstr "" -#: bookwyrm/templates/snippets/create_status.html:9 +#: bookwyrm/templates/snippets/create_status.html:17 msgid "Review" msgstr "" -#: bookwyrm/templates/snippets/create_status.html:15 +#: bookwyrm/templates/snippets/create_status.html:39 msgid "Quote" msgstr "" -#: bookwyrm/templates/snippets/create_status_form.html:18 +#: bookwyrm/templates/snippets/create_status_form.html:20 msgid "Comment:" msgstr "" -#: bookwyrm/templates/snippets/create_status_form.html:20 +#: bookwyrm/templates/snippets/create_status_form.html:22 msgid "Quote:" msgstr "" -#: bookwyrm/templates/snippets/create_status_form.html:22 +#: bookwyrm/templates/snippets/create_status_form.html:24 msgid "Review:" msgstr "" -#: bookwyrm/templates/snippets/create_status_form.html:29 -#: bookwyrm/templates/user/shelf.html:81 -msgid "Rating" +#: bookwyrm/templates/snippets/create_status_form.html:42 +#: bookwyrm/templates/snippets/status/layout.html:30 +#: bookwyrm/templates/snippets/status/layout.html:48 +#: bookwyrm/templates/snippets/status/layout.html:49 +msgid "Reply" msgstr "" -#: bookwyrm/templates/snippets/create_status_form.html:31 -#: bookwyrm/templates/snippets/rate_action.html:14 -#: bookwyrm/templates/snippets/stars.html:3 -msgid "No rating" -msgstr "" - -#: bookwyrm/templates/snippets/create_status_form.html:64 +#: bookwyrm/templates/snippets/create_status_form.html:67 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:16 msgid "Progress:" msgstr "" -#: bookwyrm/templates/snippets/create_status_form.html:71 +#: bookwyrm/templates/snippets/create_status_form.html:75 #: bookwyrm/templates/snippets/readthrough_form.html:22 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:30 msgid "pages" msgstr "" -#: bookwyrm/templates/snippets/create_status_form.html:72 +#: bookwyrm/templates/snippets/create_status_form.html:76 #: bookwyrm/templates/snippets/readthrough_form.html:23 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:31 msgid "percent" msgstr "" -#: bookwyrm/templates/snippets/create_status_form.html:77 +#: bookwyrm/templates/snippets/create_status_form.html:82 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:36 #, python-format msgid "of %(pages)s pages" msgstr "" -#: bookwyrm/templates/snippets/create_status_form.html:89 +#: bookwyrm/templates/snippets/create_status_form.html:97 msgid "Include spoiler alert" msgstr "" -#: bookwyrm/templates/snippets/create_status_form.html:95 +#: bookwyrm/templates/snippets/create_status_form.html:104 #: bookwyrm/templates/snippets/privacy-icons.html:15 #: bookwyrm/templates/snippets/privacy-icons.html:16 #: bookwyrm/templates/snippets/privacy_select.html:19 msgid "Private" msgstr "" -#: bookwyrm/templates/snippets/create_status_form.html:102 +#: bookwyrm/templates/snippets/create_status_form.html:115 msgid "Post" msgstr "" @@ -2052,24 +2222,25 @@ msgstr "" #: bookwyrm/templates/snippets/delete_readthrough_modal.html:7 #, python-format -msgid "You are deleting this readthrough and its %(count)s associated progress updates." +msgid "" +"You are deleting this readthrough and its %(count)s associated progress " +"updates." msgstr "" #: bookwyrm/templates/snippets/delete_readthrough_modal.html:15 #: bookwyrm/templates/snippets/follow_request_buttons.html:13 +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:391 msgid "Delete" msgstr "" #: bookwyrm/templates/snippets/fav_button.html:7 -#: bookwyrm/templates/snippets/fav_button.html:8 -#: bookwyrm/templates/snippets/status/status_body.html:55 -#: bookwyrm/templates/snippets/status/status_body.html:56 -msgid "Like status" +#: bookwyrm/templates/snippets/fav_button.html:9 +msgid "Like" msgstr "" #: bookwyrm/templates/snippets/fav_button.html:15 #: bookwyrm/templates/snippets/fav_button.html:16 -msgid "Un-like status" +msgid "Un-like" msgstr "" #: bookwyrm/templates/snippets/filters_panel/filters_panel.html:7 @@ -2080,11 +2251,11 @@ msgstr "" msgid "Hide filters" msgstr "" -#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:19 +#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:22 msgid "Apply filters" msgstr "" -#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:23 +#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:26 msgid "Clear filters" msgstr "" @@ -2104,6 +2275,11 @@ msgstr "" msgid "Accept" msgstr "" +#: bookwyrm/templates/snippets/form_rate_stars.html:20 +#: bookwyrm/templates/snippets/stars.html:13 +msgid "No rating" +msgstr "" + #: bookwyrm/templates/snippets/generated_status/goal.html:1 #, python-format msgid "set a goal to read %(counter)s book in %(year)s" @@ -2113,15 +2289,19 @@ msgstr[1] "" #: bookwyrm/templates/snippets/generated_status/rating.html:3 #, python-format -msgid "Rated %(title)s: %(display_rating)s star" -msgid_plural "Rated %(title)s: %(display_rating)s stars" +msgid "" +"Rated %(title)s: %(display_rating)s star" +msgid_plural "" +"Rated %(title)s: %(display_rating)s stars" msgstr[0] "" msgstr[1] "" #: bookwyrm/templates/snippets/generated_status/review_pure_name.html:4 #, python-format -msgid "Review of \"%(book_title)s\" (%(display_rating)s star): %(review_title)s" -msgid_plural "Review of \"%(book_title)s\" (%(display_rating)s stars): %(review_title)s" +msgid "" +"Review of \"%(book_title)s\" (%(display_rating)s star): %(review_title)s" +msgid_plural "" +"Review of \"%(book_title)s\" (%(display_rating)s stars): %(review_title)s" msgstr[0] "" msgstr[1] "" @@ -2132,7 +2312,9 @@ msgstr "" #: bookwyrm/templates/snippets/goal_card.html:23 #, python-format -msgid "You can set or change your reading goal any time from your profile page" +msgid "" +"You can set or change your reading goal any time from your profile page" msgstr "" #: bookwyrm/templates/snippets/goal_form.html:9 @@ -2158,10 +2340,6 @@ msgstr "" msgid "Set goal" msgstr "" -#: bookwyrm/templates/snippets/goal_progress.html:5 -msgid "Success!" -msgstr "" - #: bookwyrm/templates/snippets/goal_progress.html:7 #, python-format msgid "%(percent)s%% complete!" @@ -2169,19 +2347,32 @@ msgstr "" #: bookwyrm/templates/snippets/goal_progress.html:10 #, python-format -msgid "You've read %(read_count)s of %(goal_count)s books." +msgid "" +"You've read %(read_count)s of %(goal_count)s books." msgstr "" #: bookwyrm/templates/snippets/goal_progress.html:12 #, python-format -msgid "%(username)s has read %(read_count)s of %(goal_count)s books." +msgid "" +"%(username)s has read %(read_count)s of %(goal_count)s " +"books." msgstr "" -#: bookwyrm/templates/snippets/pagination.html:7 +#: bookwyrm/templates/snippets/page_text.html:4 +#, python-format +msgid "page %(page)s of %(total_pages)s" +msgstr "" + +#: bookwyrm/templates/snippets/page_text.html:6 +#, python-format +msgid "page %(page)s" +msgstr "" + +#: bookwyrm/templates/snippets/pagination.html:12 msgid "Previous" msgstr "" -#: bookwyrm/templates/snippets/pagination.html:15 +#: bookwyrm/templates/snippets/pagination.html:23 msgid "Next" msgstr "" @@ -2214,7 +2405,7 @@ msgstr "" msgid "Leave a rating" msgstr "" -#: bookwyrm/templates/snippets/rate_action.html:29 +#: bookwyrm/templates/snippets/rate_action.html:19 msgid "Rate" msgstr "" @@ -2222,28 +2413,28 @@ msgstr "" msgid "Progress Updates:" msgstr "" -#: bookwyrm/templates/snippets/readthrough.html:12 +#: bookwyrm/templates/snippets/readthrough.html:14 msgid "finished" msgstr "" -#: bookwyrm/templates/snippets/readthrough.html:15 +#: bookwyrm/templates/snippets/readthrough.html:25 msgid "Show all updates" msgstr "" -#: bookwyrm/templates/snippets/readthrough.html:31 +#: bookwyrm/templates/snippets/readthrough.html:41 msgid "Delete this progress update" msgstr "" -#: bookwyrm/templates/snippets/readthrough.html:41 +#: bookwyrm/templates/snippets/readthrough.html:51 msgid "started" msgstr "" -#: bookwyrm/templates/snippets/readthrough.html:47 -#: bookwyrm/templates/snippets/readthrough.html:61 +#: bookwyrm/templates/snippets/readthrough.html:57 +#: bookwyrm/templates/snippets/readthrough.html:71 msgid "Edit read dates" msgstr "" -#: bookwyrm/templates/snippets/readthrough.html:51 +#: bookwyrm/templates/snippets/readthrough.html:61 msgid "Delete these read dates" msgstr "" @@ -2271,29 +2462,25 @@ msgid "Report" msgstr "" #: bookwyrm/templates/snippets/rss_title.html:5 -#: bookwyrm/templates/snippets/status/status_header.html:11 +#: bookwyrm/templates/snippets/status/status_header.html:34 msgid "rated" msgstr "" #: bookwyrm/templates/snippets/rss_title.html:7 -#: bookwyrm/templates/snippets/status/status_header.html:13 +#: bookwyrm/templates/snippets/status/status_header.html:36 msgid "reviewed" msgstr "" #: bookwyrm/templates/snippets/rss_title.html:9 -#: bookwyrm/templates/snippets/status/status_header.html:15 +#: bookwyrm/templates/snippets/status/status_header.html:38 msgid "commented on" msgstr "" #: bookwyrm/templates/snippets/rss_title.html:11 -#: bookwyrm/templates/snippets/status/status_header.html:17 +#: bookwyrm/templates/snippets/status/status_header.html:40 msgid "quoted" msgstr "" -#: bookwyrm/templates/snippets/search_result_text.html:10 -msgid "No cover" -msgstr "" - #: bookwyrm/templates/snippets/search_result_text.html:22 #, python-format msgid "by %(author)s" @@ -2313,7 +2500,7 @@ msgid "Finish \"%(book_title)s\"" msgstr "" #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:5 -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:35 +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:34 msgid "Update progress" msgstr "" @@ -2334,7 +2521,7 @@ msgstr "" msgid "Want to read" msgstr "" -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:48 +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:45 #, python-format msgid "Remove from %(name)s" msgstr "" @@ -2349,53 +2536,44 @@ msgstr "" msgid "Want to Read \"%(book_title)s\"" msgstr "" +#: bookwyrm/templates/snippets/status/content_status.html:67 +#: bookwyrm/templates/snippets/trimmed_text.html:14 +msgid "Show more" +msgstr "" + +#: bookwyrm/templates/snippets/status/content_status.html:82 +#: bookwyrm/templates/snippets/trimmed_text.html:29 +msgid "Show less" +msgstr "" + +#: bookwyrm/templates/snippets/status/content_status.html:112 +msgid "Open image in new window" +msgstr "" + +#: bookwyrm/templates/snippets/status/layout.html:22 +#: bookwyrm/templates/snippets/status/status_options.html:17 +msgid "Delete status" +msgstr "" + +#: bookwyrm/templates/snippets/status/layout.html:52 +#: bookwyrm/templates/snippets/status/layout.html:53 +msgid "Boost status" +msgstr "" + +#: bookwyrm/templates/snippets/status/layout.html:56 +#: bookwyrm/templates/snippets/status/layout.html:57 +msgid "Like status" +msgstr "" + #: bookwyrm/templates/snippets/status/status.html:9 msgid "boosted" msgstr "" -#: bookwyrm/templates/snippets/status/status_body.html:27 -#: bookwyrm/templates/snippets/status/status_options.html:18 -msgid "Delete status" -msgstr "" - -#: bookwyrm/templates/snippets/status/status_body.html:34 -#: bookwyrm/templates/snippets/status/status_body.html:47 -#: bookwyrm/templates/snippets/status/status_body.html:48 -msgid "Reply" -msgstr "" - -#: bookwyrm/templates/snippets/status/status_content.html:18 -#: bookwyrm/templates/snippets/trimmed_text.html:15 -msgid "Show more" -msgstr "" - -#: bookwyrm/templates/snippets/status/status_content.html:25 -#: bookwyrm/templates/snippets/trimmed_text.html:25 -msgid "Show less" -msgstr "" - -#: bookwyrm/templates/snippets/status/status_content.html:46 -msgid "Open image in new window" -msgstr "" - -#: bookwyrm/templates/snippets/status/status_header.html:22 +#: bookwyrm/templates/snippets/status/status_header.html:44 #, python-format -msgid "replied to %(username)s's review" -msgstr "" - -#: bookwyrm/templates/snippets/status/status_header.html:24 -#, python-format -msgid "replied to %(username)s's comment" -msgstr "" - -#: bookwyrm/templates/snippets/status/status_header.html:26 -#, python-format -msgid "replied to %(username)s's quote" -msgstr "" - -#: bookwyrm/templates/snippets/status/status_header.html:28 -#, python-format -msgid "replied to %(username)s's status" +msgid "" +"replied to %(username)s's status" msgstr "" #: bookwyrm/templates/snippets/status/status_options.html:7 @@ -2403,6 +2581,16 @@ msgstr "" msgid "More options" msgstr "" +#: bookwyrm/templates/snippets/status/status_options.html:26 +msgid "Delete & re-draft" +msgstr "" + +#: bookwyrm/templates/snippets/status/status_options.html:35 +#: bookwyrm/templates/snippets/user_options.html:13 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:6 +msgid "Send direct message" +msgstr "" + #: bookwyrm/templates/snippets/switch_edition_button.html:5 msgid "Switch to this edition" msgstr "" @@ -2415,19 +2603,6 @@ msgstr "" msgid "Sorted descending" msgstr "" -#: bookwyrm/templates/snippets/tag.html:14 -msgid "Remove tag" -msgstr "" - -#: bookwyrm/templates/snippets/tag.html:18 -msgid "Add tag" -msgstr "" - -#: bookwyrm/templates/tag.html:9 -#, python-format -msgid "Books tagged \"%(tag.name)s\"" -msgstr "" - #: bookwyrm/templates/user/books_header.html:5 #, python-format msgid "%(username)s's books" @@ -2466,10 +2641,6 @@ msgstr "" msgid "%(username)s isn't following any users" msgstr "" -#: bookwyrm/templates/user/lists.html:9 -msgid "Your Lists" -msgstr "" - #: bookwyrm/templates/user/lists.html:11 #, python-format msgid "Lists: %(username)s" @@ -2479,7 +2650,7 @@ msgstr "" msgid "Create list" msgstr "" -#: bookwyrm/templates/user/shelf.html:24 bookwyrm/views/shelf.py:56 +#: bookwyrm/templates/user/shelf.html:24 bookwyrm/views/shelf.py:51 msgid "All books" msgstr "" @@ -2491,23 +2662,23 @@ msgstr "" msgid "Edit shelf" msgstr "" -#: bookwyrm/templates/user/shelf.html:78 +#: bookwyrm/templates/user/shelf.html:77 bookwyrm/templates/user/shelf.html:99 msgid "Shelved" msgstr "" -#: bookwyrm/templates/user/shelf.html:79 +#: bookwyrm/templates/user/shelf.html:78 bookwyrm/templates/user/shelf.html:103 msgid "Started" msgstr "" -#: bookwyrm/templates/user/shelf.html:80 +#: bookwyrm/templates/user/shelf.html:79 bookwyrm/templates/user/shelf.html:106 msgid "Finished" msgstr "" -#: bookwyrm/templates/user/shelf.html:127 +#: bookwyrm/templates/user/shelf.html:132 msgid "This shelf is empty." msgstr "" -#: bookwyrm/templates/user/shelf.html:133 +#: bookwyrm/templates/user/shelf.html:138 msgid "Delete shelf" msgstr "" @@ -2570,6 +2741,73 @@ msgstr[1] "" msgid "%(counter)s following" msgstr "" +#: bookwyrm/templates/user_admin/user.html:11 +msgid "Back to users" +msgstr "" + +#: bookwyrm/templates/user_admin/user_admin.html:7 +#, python-format +msgid "Users: %(server_name)s" +msgstr "" + +#: bookwyrm/templates/user_admin/user_admin.html:22 +#: bookwyrm/templates/user_admin/username_filter.html:5 +msgid "Username" +msgstr "" + +#: bookwyrm/templates/user_admin/user_admin.html:26 +msgid "Date Added" +msgstr "" + +#: bookwyrm/templates/user_admin/user_admin.html:30 +msgid "Last Active" +msgstr "" + +#: bookwyrm/templates/user_admin/user_admin.html:38 +msgid "Remote server" +msgstr "" + +#: bookwyrm/templates/user_admin/user_admin.html:47 +msgid "Active" +msgstr "" + +#: bookwyrm/templates/user_admin/user_admin.html:47 +msgid "Inactive" +msgstr "" + +#: bookwyrm/templates/user_admin/user_admin.html:52 +#: bookwyrm/templates/user_admin/user_info.html:49 +msgid "Not set" +msgstr "" + +#: bookwyrm/templates/user_admin/user_info.html:5 +msgid "User details" +msgstr "" + +#: bookwyrm/templates/user_admin/user_info.html:14 +msgid "View user profile" +msgstr "" + +#: bookwyrm/templates/user_admin/user_info.html:20 +msgid "Instance details" +msgstr "" + +#: bookwyrm/templates/user_admin/user_info.html:46 +msgid "View instance" +msgstr "" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:11 +msgid "Suspend user" +msgstr "" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:13 +msgid "Un-suspend user" +msgstr "" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:21 +msgid "Access level:" +msgstr "" + #: bookwyrm/views/password.py:32 msgid "No user with that email address was found." msgstr "" @@ -2578,3 +2816,1299 @@ msgstr "" #, python-format msgid "A password reset link sent to %s" msgstr "" + +#: venv3/lib/python3.8/site-packages/_pytest/config/argparsing.py:442 +#, python-format +msgid "ambiguous option: %(option)s could match %(matches)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/contrib/messages/apps.py:7 +msgid "Messages" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/contrib/sitemaps/apps.py:7 +msgid "Site Maps" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/contrib/staticfiles/apps.py:9 +msgid "Static Files" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/contrib/syndication/apps.py:7 +msgid "Syndication" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/paginator.py:45 +msgid "That page number is not an integer" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/paginator.py:47 +msgid "That page number is less than 1" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/paginator.py:52 +msgid "That page contains no results" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:32 +msgid "Enter a valid value." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:103 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:659 +msgid "Enter a valid URL." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:155 +msgid "Enter a valid integer." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:166 +msgid "Enter a valid email address." +msgstr "" + +#. Translators: "letters" means latin letters: a-z and A-Z. +#: venv3/lib/python3.8/site-packages/django/core/validators.py:240 +msgid "" +"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:247 +msgid "" +"Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " +"hyphens." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:256 +#: venv3/lib/python3.8/site-packages/django/core/validators.py:276 +msgid "Enter a valid IPv4 address." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:261 +#: venv3/lib/python3.8/site-packages/django/core/validators.py:277 +msgid "Enter a valid IPv6 address." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:271 +#: venv3/lib/python3.8/site-packages/django/core/validators.py:275 +msgid "Enter a valid IPv4 or IPv6 address." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:305 +msgid "Enter only digits separated by commas." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:311 +#, python-format +msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:343 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:352 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:362 +#, python-format +msgid "" +"Ensure this value has at least %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at least %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:377 +#, python-format +msgid "" +"Ensure this value has at most %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at most %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:396 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:291 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:326 +msgid "Enter a number." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:398 +#, python-format +msgid "Ensure that there are no more than %(max)s digit in total." +msgid_plural "Ensure that there are no more than %(max)s digits in total." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:403 +#, python-format +msgid "Ensure that there are no more than %(max)s decimal place." +msgid_plural "Ensure that there are no more than %(max)s decimal places." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:408 +#, python-format +msgid "" +"Ensure that there are no more than %(max)s digit before the decimal point." +msgid_plural "" +"Ensure that there are no more than %(max)s digits before the decimal point." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:470 +#, python-format +msgid "" +"File extension “%(extension)s” is not allowed. Allowed extensions are: " +"%(allowed_extensions)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:522 +msgid "Null characters are not allowed." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/base.py:1181 +#: venv3/lib/python3.8/site-packages/django/forms/models.py:759 +msgid "and" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/base.py:1183 +#, python-format +msgid "%(model_name)s with this %(field_labels)s already exists." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:104 +#, python-format +msgid "Value %(value)r is not a valid choice." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:105 +msgid "This field cannot be null." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:106 +msgid "This field cannot be blank." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:107 +#, python-format +msgid "%(model_name)s with this %(field_label)s already exists." +msgstr "" + +#. Translators: The 'lookup_type' is one of 'date', 'year' or 'month'. +#. Eg: "Title must be unique for pub_date year" +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:111 +#, python-format +msgid "" +"%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:130 +#, python-format +msgid "Field of type: %(field_type)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:937 +#, python-format +msgid "“%(value)s” value must be either True or False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:938 +#, python-format +msgid "“%(value)s” value must be either True, False, or None." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:940 +msgid "Boolean (Either True or False)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:981 +#, python-format +msgid "String (up to %(max_length)s)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1045 +msgid "Comma-separated integers" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1094 +#, python-format +msgid "" +"“%(value)s” value has an invalid date format. It must be in YYYY-MM-DD " +"format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1096 +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1239 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD) but it is an invalid " +"date." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1099 +msgid "Date (without time)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1237 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." +"uuuuuu]][TZ] format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1241 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" +"[TZ]) but it is an invalid date/time." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1245 +msgid "Date (with time)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1393 +#, python-format +msgid "“%(value)s” value must be a decimal number." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1395 +msgid "Decimal number" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1534 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in [DD] [[HH:]MM:]ss[." +"uuuuuu] format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1537 +msgid "Duration" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1587 +msgid "Email address" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1610 +msgid "File path" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1676 +#, python-format +msgid "“%(value)s” value must be a float." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1678 +msgid "Floating point number" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1716 +#, python-format +msgid "“%(value)s” value must be an integer." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1718 +msgid "Integer" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1801 +msgid "Big (8 byte) integer" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1817 +msgid "IPv4 address" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1848 +msgid "IP address" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1928 +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1929 +#, python-format +msgid "“%(value)s” value must be either None, True or False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1931 +msgid "Boolean (Either True, False or None)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1966 +msgid "Positive integer" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1979 +msgid "Positive small integer" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1993 +#, python-format +msgid "Slug (up to %(max_length)s)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2025 +msgid "Small integer" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2032 +msgid "Text" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2060 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] " +"format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2062 +#, python-format +msgid "" +"“%(value)s” value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an " +"invalid time." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2065 +msgid "Time" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2191 +msgid "URL" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2213 +msgid "Raw binary data" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2278 +#, python-format +msgid "“%(value)s” is not a valid UUID." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2280 +msgid "Universally unique identifier" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/files.py:221 +msgid "File" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/files.py:360 +msgid "Image" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:778 +#, python-format +msgid "%(model)s instance with %(field)s %(value)r does not exist." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:780 +msgid "Foreign Key (type determined by related field)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1012 +msgid "One-to-one relationship" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1066 +#, python-format +msgid "%(from)s-%(to)s relationship" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1067 +#, python-format +msgid "%(from)s-%(to)s relationships" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1109 +msgid "Many-to-many relationship" +msgstr "" + +#. Translators: If found as last label character, these punctuation +#. characters will prevent the default label_suffix to be appended to the label +#: venv3/lib/python3.8/site-packages/django/forms/boundfield.py:149 +msgid ":?.!" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:53 +msgid "This field is required." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:246 +msgid "Enter a whole number." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:397 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:1127 +msgid "Enter a valid date." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:421 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:1128 +msgid "Enter a valid time." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:443 +msgid "Enter a valid date/time." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:472 +msgid "Enter a valid duration." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:473 +#, python-brace-format +msgid "The number of days must be between {min_days} and {max_days}." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:533 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:534 +msgid "No file was submitted." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:535 +msgid "The submitted file is empty." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:537 +#, python-format +msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." +msgid_plural "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:540 +msgid "Please either submit a file or check the clear checkbox, not both." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:601 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:763 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:853 +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1275 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:854 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:969 +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1274 +msgid "Enter a list of values." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:970 +msgid "Enter a complete value." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:1186 +msgid "Enter a valid UUID." +msgstr "" + +#. Translators: This is the default suffix added to form field labels +#: venv3/lib/python3.8/site-packages/django/forms/forms.py:81 +msgid ":" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/forms.py:207 +#, python-format +msgid "(Hidden field %(name)s) %(error)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:93 +msgid "ManagementForm data is missing or has been tampered with" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:345 +#, python-format +msgid "Please submit %d or fewer forms." +msgid_plural "Please submit %d or fewer forms." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:352 +#, python-format +msgid "Please submit %d or more forms." +msgid_plural "Please submit %d or more forms." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:379 +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:386 +msgid "Order" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:754 +#, python-format +msgid "Please correct the duplicate data for %(field)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:758 +#, python-format +msgid "Please correct the duplicate data for %(field)s, which must be unique." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:764 +#, python-format +msgid "" +"Please correct the duplicate data for %(field_name)s which must be unique " +"for the %(lookup)s in %(date_field)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:773 +msgid "Please correct the duplicate values below." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1094 +msgid "The inline value did not match the parent instance." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1161 +msgid "Select a valid choice. That choice is not one of the available choices." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1277 +#, python-format +msgid "“%(pk)s” is not a valid value." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/utils.py:162 +#, python-format +msgid "" +"%(datetime)s couldn’t be interpreted in time zone %(current_timezone)s; it " +"may be ambiguous or it may not exist." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:395 +msgid "Clear" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:396 +msgid "Currently" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:397 +msgid "Change" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:709 +msgid "Unknown" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:710 +msgid "Yes" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:711 +msgid "No" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:788 +msgid "yes,no,maybe" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:817 +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:834 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:836 +#, python-format +msgid "%s KB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:838 +#, python-format +msgid "%s MB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:840 +#, python-format +msgid "%s GB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:842 +#, python-format +msgid "%s TB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:844 +#, python-format +msgid "%s PB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:62 +msgid "p.m." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:63 +msgid "a.m." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:68 +msgid "PM" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:69 +msgid "AM" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:150 +msgid "midnight" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:152 +msgid "noon" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:295 +msgid "Monday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:296 +msgid "Tuesday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:297 +msgid "Wednesday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:298 +msgid "Thursday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:299 +msgid "Friday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:7 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:300 +msgid "Saturday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:7 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:301 +msgid "Sunday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Mon" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Tue" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Wed" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Thu" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Fri" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:11 +msgid "Sat" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:11 +msgid "Sun" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:281 +msgid "January" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:282 +msgid "February" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:283 +msgid "March" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:284 +msgid "April" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:285 +msgid "May" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:286 +msgid "June" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:287 +msgid "July" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:288 +msgid "August" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:289 +msgid "September" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:290 +msgid "October" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:291 +msgid "November" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:16 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:292 +msgid "December" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "jan" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "feb" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "mar" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "apr" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "may" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "jun" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "jul" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "aug" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "sep" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "oct" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "nov" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "dec" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:23 +msgctxt "abbrev. month" +msgid "Jan." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:24 +msgctxt "abbrev. month" +msgid "Feb." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:25 +msgctxt "abbrev. month" +msgid "March" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:26 +msgctxt "abbrev. month" +msgid "April" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:27 +msgctxt "abbrev. month" +msgid "May" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:28 +msgctxt "abbrev. month" +msgid "June" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:29 +msgctxt "abbrev. month" +msgid "July" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:30 +msgctxt "abbrev. month" +msgid "Aug." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:31 +msgctxt "abbrev. month" +msgid "Sept." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:32 +msgctxt "abbrev. month" +msgid "Oct." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:33 +msgctxt "abbrev. month" +msgid "Nov." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:34 +msgctxt "abbrev. month" +msgid "Dec." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:37 +msgctxt "alt. month" +msgid "January" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:38 +msgctxt "alt. month" +msgid "February" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:39 +msgctxt "alt. month" +msgid "March" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:40 +msgctxt "alt. month" +msgid "April" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:41 +msgctxt "alt. month" +msgid "May" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:42 +msgctxt "alt. month" +msgid "June" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:43 +msgctxt "alt. month" +msgid "July" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:44 +msgctxt "alt. month" +msgid "August" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:45 +msgctxt "alt. month" +msgid "September" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:46 +msgctxt "alt. month" +msgid "October" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:47 +msgctxt "alt. month" +msgid "November" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:48 +msgctxt "alt. month" +msgid "December" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/ipv6.py:8 +msgid "This is not a valid IPv6 address." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/text.py:69 +#, python-format +msgctxt "String to return when truncating text" +msgid "%(truncated_text)s…" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/text.py:235 +msgid "or" +msgstr "" + +#. Translators: This string is used as a separator between list elements +#: venv3/lib/python3.8/site-packages/django/utils/text.py:254 +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:83 +msgid ", " +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:9 +#, python-format +msgid "%d year" +msgid_plural "%d years" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:10 +#, python-format +msgid "%d month" +msgid_plural "%d months" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:11 +#, python-format +msgid "%d week" +msgid_plural "%d weeks" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:12 +#, python-format +msgid "%d day" +msgid_plural "%d days" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:13 +#, python-format +msgid "%d hour" +msgid_plural "%d hours" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:14 +#, python-format +msgid "%d minute" +msgid_plural "%d minutes" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:72 +msgid "0 minutes" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:110 +msgid "Forbidden" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:111 +msgid "CSRF verification failed. Request aborted." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:115 +msgid "" +"You are seeing this message because this HTTPS site requires a “Referer " +"header” to be sent by your Web browser, but none was sent. This header is " +"required for security reasons, to ensure that your browser is not being " +"hijacked by third parties." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:120 +msgid "" +"If you have configured your browser to disable “Referer” headers, please re-" +"enable them, at least for this site, or for HTTPS connections, or for “same-" +"origin” requests." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:124 +msgid "" +"If you are using the tag or " +"including the “Referrer-Policy: no-referrer” header, please remove them. The " +"CSRF protection requires the “Referer” header to do strict referer checking. " +"If you’re concerned about privacy, use alternatives like for links to third-party sites." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:132 +msgid "" +"You are seeing this message because this site requires a CSRF cookie when " +"submitting forms. This cookie is required for security reasons, to ensure " +"that your browser is not being hijacked by third parties." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:137 +msgid "" +"If you have configured your browser to disable cookies, please re-enable " +"them, at least for this site, or for “same-origin” requests." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:142 +msgid "More information is available with DEBUG=True." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:41 +msgid "No year specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:61 +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:111 +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:208 +msgid "Date out of range" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:90 +msgid "No month specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:142 +msgid "No day specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:188 +msgid "No week specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:338 +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:367 +#, python-format +msgid "No %(verbose_name_plural)s available" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:589 +#, python-format +msgid "" +"Future %(verbose_name_plural)s not available because %(class_name)s." +"allow_future is False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:623 +#, python-format +msgid "Invalid date string “%(datestr)s” given format “%(format)s”" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/detail.py:54 +#, python-format +msgid "No %(verbose_name)s found matching the query" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/list.py:67 +msgid "Page is not “last”, nor can it be converted to an int." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/list.py:72 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/list.py:154 +#, python-format +msgid "Empty list and “%(class_name)s.allow_empty” is False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/static.py:40 +msgid "Directory indexes are not allowed here." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/static.py:42 +#, python-format +msgid "“%(path)s” does not exist" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/static.py:80 +#, python-format +msgid "Index of %(directory)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:7 +msgid "Django: the Web framework for perfectionists with deadlines." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:346 +#, python-format +msgid "" +"View release notes for Django %(version)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:368 +msgid "The install worked successfully! Congratulations!" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:369 +#, python-format +msgid "" +"You are seeing this page because DEBUG=True is in your settings file and you have not configured any " +"URLs." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:384 +msgid "Django Documentation" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:385 +msgid "Topics, references, & how-to’s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:396 +msgid "Tutorial: A Polling App" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:397 +msgid "Get started with Django" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:408 +msgid "Django Community" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:409 +msgid "Connect, get help, or contribute" +msgstr "" + +#: venv3/lib/python3.8/site-packages/kombu/transport/qpid.py:1301 +#, python-format +msgid "Attempting to connect to qpid with SASL mechanism %s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/kombu/transport/qpid.py:1306 +#, python-format +msgid "Connected to qpid with SASL mechanism %s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/kombu/transport/qpid.py:1324 +#, python-format +msgid "Unable to connect to qpid with SASL mechanism %s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:371 +msgid "1 second ago" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:377 +msgid "1 minute ago" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:382 +msgid "1 hour ago" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:385 +#, python-format +msgid "%(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:387 +msgid "yesterday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:387 +#, python-format +msgid "yesterday at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:389 +#, python-format +msgid "%(weekday)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:389 +#, python-format +msgid "%(weekday)s at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:392 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:445 +#, python-format +msgid "%(month_name)s %(day)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:394 +#, python-format +msgid "%(month_name)s %(day)s at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:399 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:401 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:439 +#, python-format +msgid "%(weekday)s, %(month_name)s %(day)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:462 +#, python-format +msgid "%(commas)s and %(last)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:68 +msgctxt "law" +msgid "right" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:69 +msgctxt "good" +msgid "right" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:71 +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:74 +msgctxt "organization" +msgid "club" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:76 +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:77 +msgctxt "stick" +msgid "club" +msgstr "" diff --git a/locale/es/LC_MESSAGES/django.mo b/locale/es/LC_MESSAGES/django.mo index ea8eac7e..64baee4f 100644 Binary files a/locale/es/LC_MESSAGES/django.mo and b/locale/es/LC_MESSAGES/django.mo differ diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index 64921d5e..7994ce69 100644 --- a/locale/es/LC_MESSAGES/django.po +++ b/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-04-01 13:14-0700\n" +"POT-Creation-Date: 2021-04-26 09:56-0700\n" "PO-Revision-Date: 2021-03-19 11:49+0800\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,37 +18,60 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: bookwyrm/forms.py:226 -#, fuzzy -#| msgid "A user with that username already exists." +#: bookwyrm/forms.py:224 msgid "A user with this email already exists." -msgstr "Ya existe un usuario con ese nombre." +msgstr "Ya existe un usuario con ese correo electrónico." -#: bookwyrm/forms.py:240 +#: bookwyrm/forms.py:238 msgid "One Day" msgstr "Un día" -#: bookwyrm/forms.py:241 +#: bookwyrm/forms.py:239 msgid "One Week" msgstr "Una semana" -#: bookwyrm/forms.py:242 +#: bookwyrm/forms.py:240 msgid "One Month" msgstr "Un mes" -#: bookwyrm/forms.py:243 +#: bookwyrm/forms.py:241 msgid "Does Not Expire" msgstr "Nunca se vence" -#: bookwyrm/forms.py:248 +#: bookwyrm/forms.py:246 #, python-format msgid "%(count)d uses" msgstr "%(count)d usos" -#: bookwyrm/forms.py:251 +#: bookwyrm/forms.py:249 msgid "Unlimited" msgstr "Sin límite" +#: bookwyrm/forms.py:293 +msgid "List Order" +msgstr "Orden de la lista" + +#: bookwyrm/forms.py:294 +msgid "Book Title" +msgstr "Título" + +#: bookwyrm/forms.py:295 bookwyrm/templates/snippets/create_status_form.html:31 +#: bookwyrm/templates/user/shelf.html:80 bookwyrm/templates/user/shelf.html:110 +msgid "Rating" +msgstr "Calificación" + +#: bookwyrm/forms.py:297 bookwyrm/templates/lists/list.html:82 +msgid "Sort By" +msgstr "Ordenar por" + +#: bookwyrm/forms.py:301 +msgid "Ascending" +msgstr "Ascendente" + +#: bookwyrm/forms.py:302 +msgid "Descending" +msgstr "Descendente" + #: bookwyrm/models/fields.py:24 #, python-format msgid "%(value)s is not a valid remote_id" @@ -59,7 +82,7 @@ msgstr "%(value)s no es un remote_id válido" msgid "%(value)s is not a valid username" msgstr "%(value)s no es un usuario válido" -#: bookwyrm/models/fields.py:165 bookwyrm/templates/layout.html:157 +#: bookwyrm/models/fields.py:165 bookwyrm/templates/layout.html:153 msgid "username" msgstr "nombre de usuario" @@ -67,23 +90,23 @@ msgstr "nombre de usuario" msgid "A user with that username already exists." msgstr "Ya existe un usuario con ese nombre." -#: bookwyrm/settings.py:150 +#: bookwyrm/settings.py:152 msgid "English" msgstr "Inglés" -#: bookwyrm/settings.py:151 +#: bookwyrm/settings.py:153 msgid "German" msgstr "Aléman" -#: bookwyrm/settings.py:152 +#: bookwyrm/settings.py:154 msgid "Spanish" msgstr "Español" -#: bookwyrm/settings.py:153 +#: bookwyrm/settings.py:155 msgid "French" msgstr "Francés" -#: bookwyrm/settings.py:154 +#: bookwyrm/settings.py:156 msgid "Simplified Chinese" msgstr "Chino simplificado" @@ -120,82 +143,70 @@ msgstr "Wikipedia" msgid "Books by %(name)s" msgstr "Libros de %(name)s" -#: bookwyrm/templates/book/book.html:21 +#: bookwyrm/templates/book/book.html:33 #: bookwyrm/templates/discover/large-book.html:12 #: bookwyrm/templates/discover/small-book.html:9 msgid "by" msgstr "por" -#: bookwyrm/templates/book/book.html:29 bookwyrm/templates/book/book.html:30 +#: bookwyrm/templates/book/book.html:41 bookwyrm/templates/book/book.html:42 msgid "Edit Book" msgstr "Editar Libro" -#: bookwyrm/templates/book/book.html:49 +#: bookwyrm/templates/book/book.html:61 #: bookwyrm/templates/book/cover_modal.html:5 msgid "Add cover" msgstr "Agregar portada" -#: bookwyrm/templates/book/book.html:53 -#, fuzzy -#| msgid "Failed to load" +#: bookwyrm/templates/book/book.html:65 msgid "Failed to load cover" -msgstr "Se falló a cargar" +msgstr "No se pudo cargar la portada" -#: bookwyrm/templates/book/book.html:62 -msgid "ISBN:" -msgstr "ISBN:" - -#: bookwyrm/templates/book/book.html:69 -#: bookwyrm/templates/book/edit_book.html:211 -msgid "OCLC Number:" -msgstr "Número OCLC:" - -#: bookwyrm/templates/book/book.html:76 -#: bookwyrm/templates/book/edit_book.html:215 -msgid "ASIN:" -msgstr "ASIN:" - -#: bookwyrm/templates/book/book.html:85 +#: bookwyrm/templates/book/book.html:82 msgid "View on OpenLibrary" msgstr "Ver en OpenLibrary" -#: bookwyrm/templates/book/book.html:94 +#: bookwyrm/templates/book/book.html:102 #, python-format msgid "(%(review_count)s review)" msgid_plural "(%(review_count)s reviews)" msgstr[0] "(%(review_count)s reseña)" msgstr[1] "(%(review_count)s reseñas)" -#: bookwyrm/templates/book/book.html:100 +#: bookwyrm/templates/book/book.html:114 msgid "Add Description" msgstr "Agregar descripción" -#: bookwyrm/templates/book/book.html:107 -#: bookwyrm/templates/book/edit_book.html:101 +#: bookwyrm/templates/book/book.html:121 +#: bookwyrm/templates/book/edit_book.html:107 #: bookwyrm/templates/lists/form.html:12 msgid "Description:" msgstr "Descripción:" -#: bookwyrm/templates/book/book.html:111 -#: bookwyrm/templates/book/edit_book.html:225 +#: bookwyrm/templates/book/book.html:125 +#: bookwyrm/templates/book/edit_book.html:240 #: bookwyrm/templates/edit_author.html:78 bookwyrm/templates/lists/form.html:42 #: bookwyrm/templates/preferences/edit_user.html:70 +#: bookwyrm/templates/settings/edit_server.html:68 +#: bookwyrm/templates/settings/federated_server.html:93 #: bookwyrm/templates/settings/site.html:93 -#: bookwyrm/templates/snippets/readthrough.html:65 +#: bookwyrm/templates/snippets/readthrough.html:75 #: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:42 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:42 #: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:34 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:38 msgid "Save" msgstr "Guardar" -#: bookwyrm/templates/book/book.html:112 bookwyrm/templates/book/book.html:161 +#: bookwyrm/templates/book/book.html:126 bookwyrm/templates/book/book.html:175 #: bookwyrm/templates/book/cover_modal.html:32 -#: bookwyrm/templates/book/edit_book.html:226 +#: bookwyrm/templates/book/edit_book.html:241 #: bookwyrm/templates/edit_author.html:79 -#: bookwyrm/templates/moderation/report_modal.html:32 +#: bookwyrm/templates/moderation/report_modal.html:34 +#: bookwyrm/templates/settings/federated_server.html:94 #: bookwyrm/templates/snippets/delete_readthrough_modal.html:17 #: bookwyrm/templates/snippets/goal_form.html:32 -#: bookwyrm/templates/snippets/readthrough.html:66 +#: bookwyrm/templates/snippets/readthrough.html:76 #: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:43 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:43 #: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:35 @@ -203,91 +214,127 @@ msgstr "Guardar" msgid "Cancel" msgstr "Cancelar" -#: bookwyrm/templates/book/book.html:121 +#: bookwyrm/templates/book/book.html:135 #, python-format msgid "%(count)s editions" msgstr "%(count)s ediciones" -#: bookwyrm/templates/book/book.html:129 +#: bookwyrm/templates/book/book.html:143 #, python-format msgid "This edition is on your %(shelf_name)s shelf." -msgstr "Esta edición está en tu %(shelf_name)s estante." +msgstr "" +"Esta edición está en tu %(shelf_name)s estante." -#: bookwyrm/templates/book/book.html:135 +#: bookwyrm/templates/book/book.html:149 #, python-format -msgid "A different edition of this book is on your %(shelf_name)s shelf." -msgstr "Una edición diferente de este libro está en tu %(shelf_name)s estante." +msgid "" +"A different edition of this book is on your %(shelf_name)s shelf." +msgstr "" +"Una edición diferente de este libro está en tu " +"%(shelf_name)s estante." -#: bookwyrm/templates/book/book.html:144 +#: bookwyrm/templates/book/book.html:158 msgid "Your reading activity" msgstr "Tu actividad de lectura" -#: bookwyrm/templates/book/book.html:146 +#: bookwyrm/templates/book/book.html:160 msgid "Add read dates" msgstr "Agregar fechas de lectura" -#: bookwyrm/templates/book/book.html:151 +#: bookwyrm/templates/book/book.html:165 msgid "You don't have any reading activity for this book." msgstr "No tienes ninguna actividad de lectura para este libro." -#: bookwyrm/templates/book/book.html:158 +#: bookwyrm/templates/book/book.html:172 msgid "Create" msgstr "Crear" -#: bookwyrm/templates/book/book.html:180 +#: bookwyrm/templates/book/book.html:194 msgid "Subjects" msgstr "Sujetos" -#: bookwyrm/templates/book/book.html:191 +#: bookwyrm/templates/book/book.html:206 msgid "Places" msgstr "Lugares" -#: bookwyrm/templates/book/book.html:202 bookwyrm/templates/layout.html:64 +#: bookwyrm/templates/book/book.html:217 bookwyrm/templates/layout.html:65 #: bookwyrm/templates/lists/lists.html:5 bookwyrm/templates/lists/lists.html:12 #: bookwyrm/templates/search_results.html:91 #: bookwyrm/templates/user/user_layout.html:62 msgid "Lists" msgstr "Listas" -#: bookwyrm/templates/book/book.html:213 -#, fuzzy -#| msgid "Go to list" +#: bookwyrm/templates/book/book.html:228 msgid "Add to list" -msgstr "Irse a lista" +msgstr "Agregar a lista" -#: bookwyrm/templates/book/book.html:223 +#: bookwyrm/templates/book/book.html:238 #: bookwyrm/templates/book/cover_modal.html:31 -#: bookwyrm/templates/lists/list.html:90 +#: bookwyrm/templates/lists/list.html:133 msgid "Add" msgstr "Agregar" -#: bookwyrm/templates/book/book.html:251 +#: bookwyrm/templates/book/book.html:254 +#, fuzzy +#| msgid "Review" +msgid "Reviews" +msgstr "Reseña" + +#: bookwyrm/templates/book/book.html:259 +#, fuzzy +#| msgid "Your shelves" +msgid "Your reviews" +msgstr "Tus estantes" + +#: bookwyrm/templates/book/book.html:265 +#, fuzzy +#| msgid "Your Account" +msgid "Your comments" +msgstr "Tu cuenta" + +#: bookwyrm/templates/book/book.html:271 +#, fuzzy +#| msgid "Your books" +msgid "Your quotes" +msgstr "Tus libros" + +#: bookwyrm/templates/book/book.html:305 msgid "rated it" msgstr "lo calificó con" +#: bookwyrm/templates/book/book_identifiers.html:8 +msgid "ISBN:" +msgstr "ISBN:" + +#: bookwyrm/templates/book/book_identifiers.html:15 +#: bookwyrm/templates/book/edit_book.html:226 +msgid "OCLC Number:" +msgstr "Número OCLC:" + +#: bookwyrm/templates/book/book_identifiers.html:22 +#: bookwyrm/templates/book/edit_book.html:230 +msgid "ASIN:" +msgstr "ASIN:" + #: bookwyrm/templates/book/cover_modal.html:17 -#: bookwyrm/templates/book/edit_book.html:163 -#, fuzzy -#| msgid "Add cover" +#: bookwyrm/templates/book/edit_book.html:178 msgid "Upload cover:" -msgstr "Agregar portada" +msgstr "Subir portada:" #: bookwyrm/templates/book/cover_modal.html:23 -#: bookwyrm/templates/book/edit_book.html:169 +#: bookwyrm/templates/book/edit_book.html:184 msgid "Load cover from url:" -msgstr "" +msgstr "Agregar portada de url:" #: bookwyrm/templates/book/edit_book.html:5 #: bookwyrm/templates/book/edit_book.html:11 -#, fuzzy, python-format -#| msgid "Editions of %(book_title)s" +#, python-format msgid "Edit \"%(book_title)s\"" -msgstr "Ediciones de %(book_title)s" +msgstr "Editar \"%(book_title)s\"" #: bookwyrm/templates/book/edit_book.html:5 #: bookwyrm/templates/book/edit_book.html:13 -#, fuzzy -#| msgid "Add Books" msgid "Add Book" msgstr "Agregar libro" @@ -308,35 +355,34 @@ msgstr "Editado más recientemente por:" #: bookwyrm/templates/book/edit_book.html:40 msgid "Confirm Book Info" -msgstr "" +msgstr "Confirmar información de libro" #: bookwyrm/templates/book/edit_book.html:47 #, python-format msgid "Is \"%(name)s\" an existing author?" -msgstr "" +msgstr "¿Es \"%(name)s\" un autor ya existente?" #: bookwyrm/templates/book/edit_book.html:52 -#, fuzzy, python-format -#| msgid "Start \"%(book_title)s\"" +#, python-format msgid "Author of %(book_title)s" -msgstr "Empezar \"%(book_title)s\"" +msgstr "Autor de %(book_title)s" #: bookwyrm/templates/book/edit_book.html:55 msgid "This is a new author" -msgstr "" +msgstr "Este es un autor nuevo" #: bookwyrm/templates/book/edit_book.html:61 #, python-format msgid "Creating a new author: %(name)s" -msgstr "" +msgstr "Creando un autor nuevo: %(name)s" #: bookwyrm/templates/book/edit_book.html:67 msgid "Is this an edition of an existing work?" -msgstr "" +msgstr "¿Es esta una edición de una obra ya existente?" #: bookwyrm/templates/book/edit_book.html:71 msgid "This is a new work" -msgstr "" +msgstr "Esta es una obra nueva" #: bookwyrm/templates/book/edit_book.html:77 #: bookwyrm/templates/password_reset.html:30 @@ -353,93 +399,86 @@ msgstr "Volver" msgid "Metadata" msgstr "Metadatos" -#: bookwyrm/templates/book/edit_book.html:91 +#: bookwyrm/templates/book/edit_book.html:92 msgid "Title:" msgstr "Título:" -#: bookwyrm/templates/book/edit_book.html:96 +#: bookwyrm/templates/book/edit_book.html:100 msgid "Subtitle:" msgstr "Subtítulo:" -#: bookwyrm/templates/book/edit_book.html:106 +#: bookwyrm/templates/book/edit_book.html:113 msgid "Series:" msgstr "Serie:" -#: bookwyrm/templates/book/edit_book.html:111 +#: bookwyrm/templates/book/edit_book.html:120 msgid "Series number:" msgstr "Número de serie:" -#: bookwyrm/templates/book/edit_book.html:117 -#, fuzzy -#| msgid "Published" +#: bookwyrm/templates/book/edit_book.html:126 msgid "Publisher:" -msgstr "Publicado" +msgstr "Editorial:" -#: bookwyrm/templates/book/edit_book.html:119 +#: bookwyrm/templates/book/edit_book.html:128 msgid "Separate multiple publishers with commas." -msgstr "" +msgstr "Separar varios editores con comas." -#: bookwyrm/templates/book/edit_book.html:125 +#: bookwyrm/templates/book/edit_book.html:135 msgid "First published date:" msgstr "Fecha de primera publicación:" -#: bookwyrm/templates/book/edit_book.html:130 +#: bookwyrm/templates/book/edit_book.html:143 msgid "Published date:" msgstr "Fecha de publicación:" -#: bookwyrm/templates/book/edit_book.html:137 -#, fuzzy -#| msgid "Author" +#: bookwyrm/templates/book/edit_book.html:152 msgid "Authors" -msgstr "Autor/Autora" +msgstr "Autores" -#: bookwyrm/templates/book/edit_book.html:143 -#, fuzzy, python-format -#| msgid "Added by %(username)s" +#: bookwyrm/templates/book/edit_book.html:158 +#, python-format msgid "Remove %(name)s" -msgstr "Agregado por %(username)s" +msgstr "Eliminar %(name)s" -#: bookwyrm/templates/book/edit_book.html:148 -#, fuzzy -#| msgid "Edit Author" +#: bookwyrm/templates/book/edit_book.html:163 msgid "Add Authors:" -msgstr "Editar Autor/Autora" +msgstr "Agregar Autores:" -#: bookwyrm/templates/book/edit_book.html:149 +#: bookwyrm/templates/book/edit_book.html:164 msgid "John Doe, Jane Smith" -msgstr "" +msgstr "Juan Nadie, Natalia Natalia" -#: bookwyrm/templates/book/edit_book.html:155 -#: bookwyrm/templates/user/shelf.html:75 +#: bookwyrm/templates/book/edit_book.html:170 +#: bookwyrm/templates/user/shelf.html:74 msgid "Cover" msgstr "Portada:" -#: bookwyrm/templates/book/edit_book.html:182 +#: bookwyrm/templates/book/edit_book.html:197 msgid "Physical Properties" msgstr "Propiedades físicas:" -#: bookwyrm/templates/book/edit_book.html:183 +#: bookwyrm/templates/book/edit_book.html:198 #: bookwyrm/templates/book/format_filter.html:5 msgid "Format:" msgstr "Formato:" -#: bookwyrm/templates/book/edit_book.html:191 +#: bookwyrm/templates/book/edit_book.html:206 msgid "Pages:" msgstr "Páginas:" -#: bookwyrm/templates/book/edit_book.html:198 +#: bookwyrm/templates/book/edit_book.html:213 msgid "Book Identifiers" msgstr "Identificadores de libro" -#: bookwyrm/templates/book/edit_book.html:199 +#: bookwyrm/templates/book/edit_book.html:214 msgid "ISBN 13:" msgstr "ISBN 13:" -#: bookwyrm/templates/book/edit_book.html:203 +#: bookwyrm/templates/book/edit_book.html:218 msgid "ISBN 10:" msgstr "ISBN 10:" -#: bookwyrm/templates/book/edit_book.html:207 +#: bookwyrm/templates/book/edit_book.html:222 #: bookwyrm/templates/edit_author.html:59 msgid "Openlibrary key:" msgstr "Clave OpenLibrary:" @@ -457,85 +496,89 @@ msgstr "Ediciones de \"%(work_title)s\"" #: bookwyrm/templates/book/format_filter.html:8 #: bookwyrm/templates/book/language_filter.html:8 msgid "Any" -msgstr "" +msgstr "Cualquier" #: bookwyrm/templates/book/language_filter.html:5 msgid "Language:" -msgstr "" +msgstr "Idioma:" -#: bookwyrm/templates/book/publisher_info.html:6 +#: bookwyrm/templates/book/publisher_info.html:22 +#, python-format +msgid "%(format)s" +msgstr "%(format)s" + +#: bookwyrm/templates/book/publisher_info.html:24 #, python-format msgid "%(format)s, %(pages)s pages" msgstr "%(format)s, %(pages)s páginas" -#: bookwyrm/templates/book/publisher_info.html:8 +#: bookwyrm/templates/book/publisher_info.html:26 #, python-format msgid "%(pages)s pages" msgstr "%(pages)s páginas" -#: bookwyrm/templates/book/publisher_info.html:13 -#, fuzzy, python-format -#| msgid "%(pages)s pages" +#: bookwyrm/templates/book/publisher_info.html:38 +#, python-format msgid "%(languages)s language" -msgstr "%(pages)s páginas" +msgstr "idioma %(languages)s" -#: bookwyrm/templates/book/publisher_info.html:18 +#: bookwyrm/templates/book/publisher_info.html:64 #, python-format msgid "Published %(date)s by %(publisher)s." -msgstr "" +msgstr "Publicado %(date)s por %(publisher)s." -#: bookwyrm/templates/book/publisher_info.html:20 -#, fuzzy, python-format -#| msgid "Published date:" +#: bookwyrm/templates/book/publisher_info.html:66 +#, python-format msgid "Published %(date)s" -msgstr "Fecha de publicación:" +msgstr "Publicado %(date)s" -#: bookwyrm/templates/book/publisher_info.html:22 +#: bookwyrm/templates/book/publisher_info.html:68 #, python-format msgid "Published by %(publisher)s." -msgstr "" +msgstr "Publicado por %(publisher)s." #: bookwyrm/templates/components/inline_form.html:8 #: bookwyrm/templates/components/modal.html:11 -#: bookwyrm/templates/feed/feed_layout.html:57 +#: bookwyrm/templates/feed/feed_layout.html:70 #: bookwyrm/templates/get_started/layout.html:19 #: bookwyrm/templates/get_started/layout.html:52 msgid "Close" msgstr "Cerrar" +#: bookwyrm/templates/compose.html:5 bookwyrm/templates/compose.html:8 +msgid "Compose status" +msgstr "Componer status" + #: bookwyrm/templates/directory/community_filter.html:5 -#, fuzzy -#| msgid "Comment" msgid "Community" -msgstr "Comentario" +msgstr "Comunidad" #: bookwyrm/templates/directory/community_filter.html:8 -#, fuzzy -#| msgid "Max uses" msgid "Local users" -msgstr "Número máximo de usos" +msgstr "Usuarios locales" #: bookwyrm/templates/directory/community_filter.html:12 -#, fuzzy -#| msgid "Federated" msgid "Federated community" -msgstr "Federalizado" +msgstr "Comunidad federalizada" #: bookwyrm/templates/directory/directory.html:6 #: bookwyrm/templates/directory/directory.html:11 -#: bookwyrm/templates/layout.html:97 +#: bookwyrm/templates/layout.html:93 msgid "Directory" -msgstr "" +msgstr "Directorio" #: bookwyrm/templates/directory/directory.html:19 msgid "Make your profile discoverable to other BookWyrm users." -msgstr "" +msgstr "Haz que tu perfil sea reconocible a otros usarios de BookWyrm." #: bookwyrm/templates/directory/directory.html:26 -#, fuzzy, python-format -#| msgid "You can set or change your reading goal any time from your profile page" -msgid "You can opt-out at any time in your profile settings." -msgstr "Puedes establecer o cambiar tu meta de lectura en cualquier momento que desees desde tu perfil" +#, python-format +msgid "" +"You can opt-out at any time in your profile settings." +msgstr "" +"Puedes optar por no en cualquier hora en tus configuraciones de perfil." #: bookwyrm/templates/directory/directory.html:31 #: bookwyrm/templates/snippets/goal_card.html:22 @@ -543,56 +586,48 @@ msgid "Dismiss message" msgstr "Desechar mensaje" #: bookwyrm/templates/directory/directory.html:71 -#, fuzzy -#| msgid "followed you" msgid "follower you follow" msgid_plural "followers you follow" -msgstr[0] "te siguió" -msgstr[1] "te siguió" +msgstr[0] "seguidor que tu sigues" +msgstr[1] "seguidores que tu sigues" #: bookwyrm/templates/directory/directory.html:78 -#, fuzzy -#| msgid "Your shelves" msgid "book on your shelves" msgid_plural "books on your shelves" -msgstr[0] "Tus estantes" -msgstr[1] "Tus estantes" +msgstr[0] "libro en tus estantes" +msgstr[1] "libro en tus estantes" #: bookwyrm/templates/directory/directory.html:86 msgid "posts" -msgstr "" +msgstr "publicaciones" #: bookwyrm/templates/directory/directory.html:92 msgid "last active" -msgstr "" +msgstr "actividad reciente" #: bookwyrm/templates/directory/sort_filter.html:5 msgid "Order by" -msgstr "" +msgstr "Ordenar por" #: bookwyrm/templates/directory/sort_filter.html:8 -#, fuzzy -#| msgid "Suggest" msgid "Suggested" -msgstr "Sugerir" +msgstr "Sugerido" #: bookwyrm/templates/directory/sort_filter.html:9 msgid "Recently active" -msgstr "" +msgstr "Activ@ recientemente" #: bookwyrm/templates/directory/user_type_filter.html:5 -#, fuzzy -#| msgid "User Activity" msgid "User type" -msgstr "Actividad de usuario" +msgstr "Tipo de usuario" #: bookwyrm/templates/directory/user_type_filter.html:8 msgid "BookWyrm users" -msgstr "" +msgstr "Usuarios de BookWyrm" #: bookwyrm/templates/directory/user_type_filter.html:12 msgid "All known users" -msgstr "" +msgstr "Todos los usuarios conocidos" #: bookwyrm/templates/discover/about.html:7 #, python-format @@ -642,11 +677,11 @@ msgstr "Esta instancia está cerrada." #: bookwyrm/templates/discover/landing_layout.html:57 msgid "Thank you! Your request has been received." -msgstr "" +msgstr "¡Gracias! Tu solicitud ha sido recibido." #: bookwyrm/templates/discover/landing_layout.html:60 msgid "Request an Invitation" -msgstr "" +msgstr "Solicitar una invitación" #: bookwyrm/templates/discover/landing_layout.html:64 #: bookwyrm/templates/password_reset_request.html:18 @@ -656,19 +691,17 @@ msgid "Email address:" msgstr "Dirección de correo electrónico:" #: bookwyrm/templates/discover/landing_layout.html:70 -#: bookwyrm/templates/moderation/report_modal.html:31 +#: bookwyrm/templates/moderation/report_modal.html:33 msgid "Submit" -msgstr "" +msgstr "Enviar" #: bookwyrm/templates/discover/landing_layout.html:79 msgid "Your Account" msgstr "Tu cuenta" #: bookwyrm/templates/edit_author.html:5 -#, fuzzy -#| msgid "Edit Author" msgid "Edit Author:" -msgstr "Editar Autor/Autora" +msgstr "Editar Autor/Autora/Autore:" #: bookwyrm/templates/edit_author.html:32 bookwyrm/templates/lists/form.html:8 #: bookwyrm/templates/user/create_shelf_form.html:13 @@ -707,49 +740,63 @@ msgstr "Clave Goodreads:" #: bookwyrm/templates/email/html_layout.html:15 #: bookwyrm/templates/email/text_layout.html:2 msgid "Hi there," -msgstr "" +msgstr "Hola, " #: bookwyrm/templates/email/html_layout.html:21 #, python-format -msgid "BookWyrm hosted on %(site_name)s" +msgid "" +"BookWyrm hosted on " +"%(site_name)s" msgstr "" +"BookWyrm alojado en " +"%(site_name)s" #: bookwyrm/templates/email/html_layout.html:23 msgid "Email preference" -msgstr "" +msgstr "Preferencia de correo electrónico" #: bookwyrm/templates/email/invite/html_content.html:6 #: bookwyrm/templates/email/invite/subject.html:2 -#, fuzzy, python-format -#| msgid "About %(site_name)s" +#, python-format msgid "You're invited to join %(site_name)s!" -msgstr "Sobre %(site_name)s" +msgstr "¡Estás invitado a unirse con %(site_name)s!" #: bookwyrm/templates/email/invite/html_content.html:9 msgid "Join Now" -msgstr "" +msgstr "Únete ahora" #: bookwyrm/templates/email/invite/html_content.html:15 #, python-format -msgid "Learn more about this instance." +msgid "" +"Learn more about this instance." msgstr "" +"Aprenda más sobre esta " +"instancia." #: bookwyrm/templates/email/invite/text_content.html:4 #, python-format -msgid "You're invited to join %(site_name)s! Click the link below to create an account." +msgid "" +"You're invited to join %(site_name)s! Click the link below to create an " +"account." msgstr "" +"Estás invitado a unirte con %(site_name)s! Haz clic en el enlace a " +"continuación para crear una cuenta." #: bookwyrm/templates/email/invite/text_content.html:8 -#, fuzzy -#| msgid "More about this site" msgid "Learn more about this instance:" -msgstr "Más sobre este sitio" +msgstr "Aprende más sobre esta intancia:" #: bookwyrm/templates/email/password_reset/html_content.html:6 #: bookwyrm/templates/email/password_reset/text_content.html:4 #, python-format -msgid "You requested to reset your %(site_name)s password. Click the link below to set a new password and log in to your account." +msgid "" +"You requested to reset your %(site_name)s password. Click the link below to " +"set a new password and log in to your account." msgstr "" +"Tú solicitaste reestablecer tu %(site_name)s contraseña. Haz clic en el " +"enlace a continuación para establecer una nueva contraseña e ingresar a tu " +"cuenta." #: bookwyrm/templates/email/password_reset/html_content.html:9 #: bookwyrm/templates/password_reset.html:4 @@ -761,14 +808,15 @@ msgstr "Restablecer contraseña" #: bookwyrm/templates/email/password_reset/html_content.html:13 #: bookwyrm/templates/email/password_reset/text_content.html:8 -msgid "If you didn't request to reset your password, you can ignore this email." +msgid "" +"If you didn't request to reset your password, you can ignore this email." msgstr "" +"Si no solicitaste reestablecer tu contraseña, puedes ignorar este mensaje." #: bookwyrm/templates/email/password_reset/subject.html:2 -#, fuzzy, python-format -#| msgid "About %(site_name)s" +#, python-format msgid "Reset your %(site_name)s password" -msgstr "Sobre %(site_name)s" +msgstr "Reestablece tu contraseña de %(site_name)s" #: bookwyrm/templates/feed/direct_messages.html:8 #, python-format @@ -776,7 +824,7 @@ msgid "Direct Messages with %(username)s" msgstr "Mensajes directos con %(username)s" #: bookwyrm/templates/feed/direct_messages.html:10 -#: bookwyrm/templates/layout.html:87 +#: bookwyrm/templates/layout.html:88 msgid "Direct Messages" msgstr "Mensajes directos" @@ -790,19 +838,15 @@ msgstr "No tienes ningún mensaje en este momento." #: bookwyrm/templates/feed/feed.html:9 msgid "Home Timeline" -msgstr "" +msgstr "Línea temporal de hogar" #: bookwyrm/templates/feed/feed.html:11 -#, fuzzy -#| msgid "%(tab_title)s Timeline" msgid "Local Timeline" -msgstr "%(tab_title)s Línea temporal" +msgstr "Línea temporal local" #: bookwyrm/templates/feed/feed.html:13 -#, fuzzy -#| msgid "Federated Servers" msgid "Federated Timeline" -msgstr "Servidores federalizados" +msgstr "Línea temporal federalizado" #: bookwyrm/templates/feed/feed.html:19 msgid "Home" @@ -813,6 +857,7 @@ msgid "Local" msgstr "Local" #: bookwyrm/templates/feed/feed.html:25 +#: bookwyrm/templates/settings/edit_server.html:40 msgid "Federated" msgstr "Federalizado" @@ -820,47 +865,50 @@ msgstr "Federalizado" #, python-format msgid "load 0 unread status(es)" msgstr "" +"cargar 0 status(es) no leídos" #: bookwyrm/templates/feed/feed.html:48 -msgid "There aren't any activities right now! Try following a user to get started" -msgstr "¡No hay actividades en este momento! Sigue a otro usuario para empezar" +msgid "" +"There aren't any activities right now! Try following a user to get started" +msgstr "¡No hay actividad ahora mismo! Sigue a otro usuario para empezar" #: bookwyrm/templates/feed/feed.html:56 #: bookwyrm/templates/get_started/users.html:6 msgid "Who to follow" -msgstr "" +msgstr "A quién seguir" #: bookwyrm/templates/feed/feed_layout.html:5 msgid "Updates" msgstr "Actualizaciones" #: bookwyrm/templates/feed/feed_layout.html:11 -#: bookwyrm/templates/layout.html:58 +#: bookwyrm/templates/layout.html:59 #: bookwyrm/templates/user/books_header.html:3 msgid "Your books" msgstr "Tus libros" #: bookwyrm/templates/feed/feed_layout.html:13 -msgid "There are no books here right now! Try searching for a book to get started" +msgid "" +"There are no books here right now! Try searching for a book to get started" msgstr "¡No hay ningún libro aqui ahorita! Busca a un libro para empezar" -#: bookwyrm/templates/feed/feed_layout.html:23 +#: bookwyrm/templates/feed/feed_layout.html:24 #: bookwyrm/templates/user/shelf.html:28 msgid "To Read" msgstr "Para leer" -#: bookwyrm/templates/feed/feed_layout.html:24 +#: bookwyrm/templates/feed/feed_layout.html:25 #: bookwyrm/templates/user/shelf.html:28 msgid "Currently Reading" msgstr "Leyendo actualmente" -#: bookwyrm/templates/feed/feed_layout.html:25 +#: bookwyrm/templates/feed/feed_layout.html:26 #: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:11 #: bookwyrm/templates/user/shelf.html:28 msgid "Read" -msgstr "Leer" +msgstr "Leido" -#: bookwyrm/templates/feed/feed_layout.html:74 bookwyrm/templates/goal.html:26 +#: bookwyrm/templates/feed/feed_layout.html:88 bookwyrm/templates/goal.html:26 #: bookwyrm/templates/snippets/goal_card.html:6 #, python-format msgid "%(year)s Reading Goal" @@ -870,30 +918,27 @@ msgstr "%(year)s Meta de lectura" #, python-format msgid "%(mutuals)s follower you follow" msgid_plural "%(mutuals)s followers you follow" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(mutuals)s seguidor que sigues" +msgstr[1] "%(mutuals)s seguidores que sigues" #: bookwyrm/templates/feed/suggested_users.html:19 #, python-format msgid "%(shared_books)s book on your shelves" msgid_plural "%(shared_books)s books on your shelves" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(shared_books)s libro en tus estantes" +msgstr[1] "%(shared_books)s libros en tus estantes" #: bookwyrm/templates/get_started/book_preview.html:6 -#, fuzzy, python-format -#| msgid "Want to Read \"%(book_title)s\"" +#, python-format msgid "Have you read %(book_title)s?" -msgstr "Quiero leer \"%(book_title)s\"" +msgstr "¿Has leído %(book_title)s?" #: bookwyrm/templates/get_started/books.html:6 -#, fuzzy -#| msgid "Started reading" msgid "What are you reading?" -msgstr "Lectura se empezó" +msgstr "¿Qué estás leyendo?" #: bookwyrm/templates/get_started/books.html:9 -#: bookwyrm/templates/lists/list.html:58 +#: bookwyrm/templates/lists/list.html:101 msgid "Search for a book" msgstr "Buscar libros" @@ -907,77 +952,65 @@ msgstr "No se encontró ningún libro correspondiente a \"%(query)s\"" #: bookwyrm/templates/get_started/books.html:11 #, python-format msgid "You can add books when you start using %(site_name)s." -msgstr "" +msgstr "Puedes agregar libros cuando comiences a usar %(site_name)s." #: bookwyrm/templates/get_started/books.html:16 #: bookwyrm/templates/get_started/books.html:17 #: bookwyrm/templates/get_started/users.html:18 #: bookwyrm/templates/get_started/users.html:19 -#: bookwyrm/templates/layout.html:37 bookwyrm/templates/layout.html:38 -#: bookwyrm/templates/lists/list.html:62 +#: bookwyrm/templates/layout.html:38 bookwyrm/templates/layout.html:39 +#: bookwyrm/templates/lists/list.html:105 msgid "Search" msgstr "Buscar" #: bookwyrm/templates/get_started/books.html:26 -#, fuzzy -#| msgid "Suggest Books" msgid "Suggested Books" -msgstr "Sugerir libros" +msgstr "Libros sugeridos" #: bookwyrm/templates/get_started/books.html:41 -#, fuzzy, python-format -#| msgid "About %(site_name)s" +#, python-format msgid "Popular on %(site_name)s" -msgstr "Sobre %(site_name)s" +msgstr "Popular en %(site_name)s" #: bookwyrm/templates/get_started/books.html:51 -#: bookwyrm/templates/lists/list.html:75 +#: bookwyrm/templates/lists/list.html:118 msgid "No books found" msgstr "No se encontró ningún libro" #: bookwyrm/templates/get_started/books.html:54 #: bookwyrm/templates/get_started/profile.html:54 msgid "Save & continue" -msgstr "" +msgstr "Guardar & continuar" #: bookwyrm/templates/get_started/layout.html:14 -#, fuzzy, python-format -#| msgid "About %(site_name)s" +#, python-format msgid "Welcome to %(site_name)s!" -msgstr "Sobre %(site_name)s" +msgstr "¡Bienvenido a %(site_name)s!" #: bookwyrm/templates/get_started/layout.html:16 msgid "These are some first steps to get you started." -msgstr "" +msgstr "Estos son unos primeros pasos para empezar." #: bookwyrm/templates/get_started/layout.html:30 #: bookwyrm/templates/get_started/profile.html:6 -#, fuzzy -#| msgid "User Profile" msgid "Create your profile" -msgstr "Perfil de usuario" +msgstr "Crear tu perfil" #: bookwyrm/templates/get_started/layout.html:34 -#, fuzzy -#| msgid "Add Books" msgid "Add books" msgstr "Agregar libros" #: bookwyrm/templates/get_started/layout.html:38 -#, fuzzy -#| msgid "Friendly" msgid "Find friends" -msgstr "Amigable" +msgstr "Encontrar amigos" #: bookwyrm/templates/get_started/layout.html:44 msgid "Skip this step" -msgstr "" +msgstr "Saltar este paso" #: bookwyrm/templates/get_started/layout.html:48 -#, fuzzy -#| msgid "Finished" msgid "Finish" -msgstr "Terminado" +msgstr "Terminar" #: bookwyrm/templates/get_started/profile.html:15 #: bookwyrm/templates/preferences/edit_user.html:24 @@ -991,7 +1024,7 @@ msgstr "Resumen:" #: bookwyrm/templates/get_started/profile.html:23 msgid "A little bit about you" -msgstr "" +msgstr "Un poco sobre ti" #: bookwyrm/templates/get_started/profile.html:32 #: bookwyrm/templates/preferences/edit_user.html:17 @@ -1006,17 +1039,19 @@ msgstr "Aprobar seguidores a mano:" #: bookwyrm/templates/get_started/profile.html:48 #: bookwyrm/templates/preferences/edit_user.html:58 msgid "Show this account in suggested users:" -msgstr "" +msgstr "Mostrar esta cuenta en los usuarios sugeridos:" #: bookwyrm/templates/get_started/profile.html:52 -msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users." +msgid "" +"Your account will show up in the directory, and may be recommended to other " +"BookWyrm users." msgstr "" +"Tu cuenta se aparecerá en el directorio, y puede ser recomendado a otros " +"usuarios de BookWyrm." #: bookwyrm/templates/get_started/users.html:11 -#, fuzzy -#| msgid "Search for a book or user" msgid "Search for a user" -msgstr "Buscar un libro o un usuario" +msgstr "Buscar un usuario" #: bookwyrm/templates/get_started/users.html:13 #: bookwyrm/templates/search_results.html:76 @@ -1036,8 +1071,12 @@ msgstr "Editar meta" #: bookwyrm/templates/goal.html:30 #: bookwyrm/templates/snippets/goal_card.html:13 #, python-format -msgid "Set a goal for how many books you'll finish reading in %(year)s, and track your progress throughout the year." -msgstr "Establece una meta para cuantos libros leerás en %(year)s, y seguir tu progreso durante el año." +msgid "" +"Set a goal for how many books you'll finish reading in %(year)s, and track " +"your progress throughout the year." +msgstr "" +"Establece una meta para cuantos libros leerás en %(year)s, y seguir tu " +"progreso durante el año." #: bookwyrm/templates/goal.html:39 #, python-format @@ -1055,19 +1094,17 @@ msgid "%(username)s's %(year)s Books" msgstr "Los libros de %(username)s para %(year)s" #: bookwyrm/templates/import.html:5 bookwyrm/templates/import.html:9 -#: bookwyrm/templates/layout.html:102 +#: bookwyrm/templates/layout.html:98 msgid "Import Books" msgstr "Importar libros" #: bookwyrm/templates/import.html:16 -#, fuzzy -#| msgid "Data source" msgid "Data source:" -msgstr "Fuente de datos" +msgstr "Fuente de datos:" #: bookwyrm/templates/import.html:29 msgid "Data file:" -msgstr "" +msgstr "Archivo de datos:" #: bookwyrm/templates/import.html:37 msgid "Include reviews" @@ -1078,6 +1115,7 @@ msgid "Privacy setting for imported reviews:" msgstr "Configuración de privacidad para las reseñas importadas:" #: bookwyrm/templates/import.html:48 +#: bookwyrm/templates/settings/server_blocklist.html:64 msgid "Import" msgstr "Importar" @@ -1116,12 +1154,16 @@ msgstr "(¡Refresca para actualizar!)" #: bookwyrm/templates/import_status.html:35 msgid "Failed to load" -msgstr "Se falló a cargar" +msgstr "No se pudo cargar" #: bookwyrm/templates/import_status.html:44 #, python-format -msgid "Jump to the bottom of the list to select the %(failed_count)s items which failed to import." +msgid "" +"Jump to the bottom of the list to select the %(failed_count)s items which " +"failed to import." msgstr "" +"Saltar al final de la lista para seleccionar los %(failed_count)s artículos " +"que no se pudieron importar." #: bookwyrm/templates/import_status.html:79 msgid "Select all" @@ -1142,12 +1184,12 @@ msgstr "Libro" #: bookwyrm/templates/import_status.html:115 #: bookwyrm/templates/snippets/create_status_form.html:10 -#: bookwyrm/templates/user/shelf.html:76 +#: bookwyrm/templates/user/shelf.html:75 bookwyrm/templates/user/shelf.html:93 msgid "Title" msgstr "Título" #: bookwyrm/templates/import_status.html:118 -#: bookwyrm/templates/user/shelf.html:77 +#: bookwyrm/templates/user/shelf.html:76 bookwyrm/templates/user/shelf.html:96 msgid "Author" msgstr "Autor/Autora" @@ -1184,105 +1226,106 @@ msgstr "Resultados de búsqueda por \"%(query)s\"" msgid "Matching Books" msgstr "Libros correspondientes" -#: bookwyrm/templates/layout.html:33 +#: bookwyrm/templates/layout.html:34 msgid "Search for a book or user" msgstr "Buscar un libro o un usuario" -#: bookwyrm/templates/layout.html:47 bookwyrm/templates/layout.html:48 +#: bookwyrm/templates/layout.html:48 bookwyrm/templates/layout.html:49 msgid "Main navigation menu" msgstr "Menú de navigación central" -#: bookwyrm/templates/layout.html:61 +#: bookwyrm/templates/layout.html:62 msgid "Feed" msgstr "Actividad" -#: bookwyrm/templates/layout.html:92 -#: bookwyrm/templates/preferences/preferences_layout.html:14 -msgid "Profile" -msgstr "Perfil" - -#: bookwyrm/templates/layout.html:107 +#: bookwyrm/templates/layout.html:103 msgid "Settings" msgstr "Configuración" -#: bookwyrm/templates/layout.html:116 -#: bookwyrm/templates/settings/admin_layout.html:24 +#: bookwyrm/templates/layout.html:112 +#: bookwyrm/templates/settings/admin_layout.html:31 #: bookwyrm/templates/settings/manage_invite_requests.html:15 #: bookwyrm/templates/settings/manage_invites.html:3 #: bookwyrm/templates/settings/manage_invites.html:15 msgid "Invites" msgstr "Invitaciones" -#: bookwyrm/templates/layout.html:123 +#: bookwyrm/templates/layout.html:119 msgid "Admin" -msgstr "" +msgstr "Admin" -#: bookwyrm/templates/layout.html:130 +#: bookwyrm/templates/layout.html:126 msgid "Log out" msgstr "Cerrar sesión" -#: bookwyrm/templates/layout.html:138 bookwyrm/templates/layout.html:139 +#: bookwyrm/templates/layout.html:134 bookwyrm/templates/layout.html:135 #: bookwyrm/templates/notifications.html:6 #: bookwyrm/templates/notifications.html:10 msgid "Notifications" msgstr "Notificaciones" -#: bookwyrm/templates/layout.html:156 bookwyrm/templates/layout.html:160 +#: bookwyrm/templates/layout.html:152 bookwyrm/templates/layout.html:156 #: bookwyrm/templates/login.html:17 #: bookwyrm/templates/snippets/register_form.html:4 msgid "Username:" msgstr "Nombre de usuario:" -#: bookwyrm/templates/layout.html:161 +#: bookwyrm/templates/layout.html:157 msgid "password" msgstr "contraseña" -#: bookwyrm/templates/layout.html:162 bookwyrm/templates/login.html:36 +#: bookwyrm/templates/layout.html:158 bookwyrm/templates/login.html:36 msgid "Forgot your password?" msgstr "¿Olvidaste tu contraseña?" -#: bookwyrm/templates/layout.html:165 bookwyrm/templates/login.html:10 +#: bookwyrm/templates/layout.html:161 bookwyrm/templates/login.html:10 #: bookwyrm/templates/login.html:33 msgid "Log in" msgstr "Iniciar sesión" -#: bookwyrm/templates/layout.html:173 +#: bookwyrm/templates/layout.html:169 msgid "Join" -msgstr "" +msgstr "Unirse" -#: bookwyrm/templates/layout.html:196 +#: bookwyrm/templates/layout.html:195 msgid "About this server" msgstr "Sobre este servidor" -#: bookwyrm/templates/layout.html:200 +#: bookwyrm/templates/layout.html:199 msgid "Contact site admin" msgstr "Contactarse con administradores del sitio" -#: bookwyrm/templates/layout.html:207 +#: bookwyrm/templates/layout.html:206 #, python-format -msgid "Support %(site_name)s on %(support_title)s" +msgid "" +"Support %(site_name)s on " +"%(support_title)s" msgstr "" +"Apoyar %(site_name)s en " +"%(support_title)s" -#: bookwyrm/templates/layout.html:211 -msgid "BookWyrm is open source software. You can contribute or report issues on GitHub." -msgstr "BookWyrm es software de código abierto. Puedes contribuir o reportar problemas en GitHub." +#: bookwyrm/templates/layout.html:210 +msgid "" +"BookWyrm is open source software. You can contribute or report issues on GitHub." +msgstr "" +"BookWyrm es software de código abierto. Puedes contribuir o reportar " +"problemas en GitHub." #: bookwyrm/templates/lists/create_form.html:5 -#: bookwyrm/templates/lists/lists.html:19 +#: bookwyrm/templates/lists/lists.html:20 msgid "Create List" msgstr "Crear lista" #: bookwyrm/templates/lists/created_text.html:5 -#, fuzzy, python-format -#| msgid "Added by %(username)s" +#, python-format msgid "Created and curated by %(username)s" -msgstr "Agregado por %(username)s" +msgstr "Agregado y comisariado por %(username)s" #: bookwyrm/templates/lists/created_text.html:7 -#, fuzzy, python-format -#| msgid "Added by %(username)s" +#, python-format msgid "Created by %(username)s" -msgstr "Agregado por %(username)s" +msgstr "Creado por %(username)s" #: bookwyrm/templates/lists/curate.html:6 msgid "Pending Books" @@ -1334,7 +1377,7 @@ msgid "Anyone can suggest books, subject to your approval" msgstr "Cualquier usuario puede sugerir libros, en cuanto lo hayas aprobado" #: bookwyrm/templates/lists/form.html:31 -#: bookwyrm/templates/moderation/reports.html:24 +#: bookwyrm/templates/moderation/reports.html:25 msgid "Open" msgstr "Abierto" @@ -1342,45 +1385,76 @@ msgstr "Abierto" msgid "Anyone can add books to this list" msgstr "Cualquer usuario puede agregar libros a esta lista" -#: bookwyrm/templates/lists/list.html:17 +#: bookwyrm/templates/lists/list.html:19 +msgid "You successfully suggested a book for this list!" +msgstr "" + +#: bookwyrm/templates/lists/list.html:21 +#, fuzzy +#| msgid "Anyone can add books to this list" +msgid "You successfully added a book to this list!" +msgstr "Cualquer usuario puede agregar libros a esta lista" + +#: bookwyrm/templates/lists/list.html:27 msgid "This list is currently empty" msgstr "Esta lista está vacia" -#: bookwyrm/templates/lists/list.html:35 +#: bookwyrm/templates/lists/list.html:46 #, python-format msgid "Added by %(username)s" msgstr "Agregado por %(username)s" -#: bookwyrm/templates/lists/list.html:41 -#: bookwyrm/templates/snippets/shelf_selector.html:28 +#: bookwyrm/templates/lists/list.html:58 +msgid "Set" +msgstr "Establecido" + +#: bookwyrm/templates/lists/list.html:61 +msgid "List position" +msgstr "Posición" + +#: bookwyrm/templates/lists/list.html:67 +#: bookwyrm/templates/snippets/shelf_selector.html:26 msgid "Remove" msgstr "Quitar" -#: bookwyrm/templates/lists/list.html:54 +#: bookwyrm/templates/lists/list.html:80 bookwyrm/templates/lists/list.html:92 +msgid "Sort List" +msgstr "Ordena la lista" + +#: bookwyrm/templates/lists/list.html:86 +msgid "Direction" +msgstr "Dirección" + +#: bookwyrm/templates/lists/list.html:97 msgid "Add Books" msgstr "Agregar libros" -#: bookwyrm/templates/lists/list.html:54 +#: bookwyrm/templates/lists/list.html:97 msgid "Suggest Books" msgstr "Sugerir libros" -#: bookwyrm/templates/lists/list.html:63 +#: bookwyrm/templates/lists/list.html:106 msgid "search" msgstr "buscar" -#: bookwyrm/templates/lists/list.html:69 +#: bookwyrm/templates/lists/list.html:112 msgid "Clear search" msgstr "Borrar búsqueda" -#: bookwyrm/templates/lists/list.html:74 +#: bookwyrm/templates/lists/list.html:117 #, python-format msgid "No books found matching the query \"%(query)s\"" -msgstr "No se encontró ningún libro correspondiente a la búsqueda: \"%(query)s\"" +msgstr "" +"No se encontró ningún libro correspondiente a la búsqueda: \"%(query)s\"" -#: bookwyrm/templates/lists/list.html:90 +#: bookwyrm/templates/lists/list.html:133 msgid "Suggest" msgstr "Sugerir" +#: bookwyrm/templates/lists/lists.html:14 bookwyrm/templates/user/lists.html:9 +msgid "Your Lists" +msgstr "Tus listas" + #: bookwyrm/templates/login.html:4 msgid "Login" msgstr "Iniciar sesión" @@ -1398,133 +1472,94 @@ msgstr "Contactar a unx administradorx para recibir una invitación" msgid "More about this site" msgstr "Más sobre este sitio" -#: bookwyrm/templates/moderation/report.html:5 #: bookwyrm/templates/moderation/report.html:6 +#: bookwyrm/templates/moderation/report.html:7 #: bookwyrm/templates/moderation/report_preview.html:6 #, python-format msgid "Report #%(report_id)s: %(username)s" -msgstr "" +msgstr "Reportar #%(report_id)s: %(username)s" -#: bookwyrm/templates/moderation/report.html:10 +#: bookwyrm/templates/moderation/report.html:11 msgid "Back to reports" -msgstr "" +msgstr "Volver a los informes" -#: bookwyrm/templates/moderation/report.html:18 -#, fuzzy -#| msgid "Notifications" -msgid "Actions" -msgstr "Notificaciones" - -#: bookwyrm/templates/moderation/report.html:19 -#, fuzzy -#| msgid "User Profile" -msgid "View user profile" -msgstr "Perfil de usuario" - -#: bookwyrm/templates/moderation/report.html:22 -#: bookwyrm/templates/snippets/status/status_options.html:25 -#: bookwyrm/templates/snippets/user_options.html:13 -msgid "Send direct message" -msgstr "Enviar mensaje directo" - -#: bookwyrm/templates/moderation/report.html:27 -msgid "Deactivate user" -msgstr "" - -#: bookwyrm/templates/moderation/report.html:29 -msgid "Reactivate user" -msgstr "" - -#: bookwyrm/templates/moderation/report.html:36 +#: bookwyrm/templates/moderation/report.html:23 msgid "Moderator Comments" -msgstr "" +msgstr "Comentarios de moderador" -#: bookwyrm/templates/moderation/report.html:54 -#: bookwyrm/templates/snippets/create_status.html:12 -#: bookwyrm/templates/snippets/create_status_form.html:52 +#: bookwyrm/templates/moderation/report.html:41 +#: bookwyrm/templates/snippets/create_status.html:28 +#: bookwyrm/templates/snippets/create_status_form.html:53 msgid "Comment" msgstr "Comentario" -#: bookwyrm/templates/moderation/report.html:59 -#, fuzzy -#| msgid "Delete status" +#: bookwyrm/templates/moderation/report.html:46 msgid "Reported statuses" -msgstr "Eliminar status" +msgstr "Statuses reportados" -#: bookwyrm/templates/moderation/report.html:61 +#: bookwyrm/templates/moderation/report.html:48 msgid "No statuses reported" -msgstr "" +msgstr "Ningún estatus reportado" -#: bookwyrm/templates/moderation/report.html:67 -msgid "Statuses has been deleted" -msgstr "" +#: bookwyrm/templates/moderation/report.html:54 +msgid "Status has been deleted" +msgstr "Status ha sido eliminado" #: bookwyrm/templates/moderation/report_modal.html:6 -#, fuzzy, python-format -#| msgid "Lists: %(username)s" +#, python-format msgid "Report @%(username)s" -msgstr "Listas: %(username)s" +msgstr "Reportar @%(username)s" -#: bookwyrm/templates/moderation/report_modal.html:21 +#: bookwyrm/templates/moderation/report_modal.html:23 #, python-format msgid "This report will be sent to %(site_name)s's moderators for review." msgstr "" +"Este informe se enviará a los moderadores de %(site_name)s para la revisión." -#: bookwyrm/templates/moderation/report_modal.html:22 -#, fuzzy -#| msgid "More about this site" +#: bookwyrm/templates/moderation/report_modal.html:24 msgid "More info about this report:" -msgstr "Más sobre este sitio" +msgstr "Más información sobre este informe:" #: bookwyrm/templates/moderation/report_preview.html:13 msgid "No notes provided" -msgstr "" +msgstr "No se proporcionó notas" #: bookwyrm/templates/moderation/report_preview.html:20 -#, fuzzy, python-format -#| msgid "Added by %(username)s" +#, python-format msgid "Reported by %(username)s" -msgstr "Agregado por %(username)s" +msgstr "Reportado por %(username)s" #: bookwyrm/templates/moderation/report_preview.html:30 msgid "Re-open" -msgstr "" +msgstr "Reabrir" #: bookwyrm/templates/moderation/report_preview.html:32 msgid "Resolve" -msgstr "" +msgstr "Resolver" #: bookwyrm/templates/moderation/reports.html:6 -#, fuzzy, python-format -#| msgid "Lists: %(username)s" +#, python-format msgid "Reports: %(server_name)s" -msgstr "Listas: %(username)s" +msgstr "Informes: %(server_name)s" #: bookwyrm/templates/moderation/reports.html:8 -#: bookwyrm/templates/moderation/reports.html:16 -#: bookwyrm/templates/settings/admin_layout.html:28 -#, fuzzy -#| msgid "Recent Imports" +#: bookwyrm/templates/moderation/reports.html:17 +#: bookwyrm/templates/settings/admin_layout.html:35 msgid "Reports" -msgstr "Importaciones recientes" +msgstr "Informes" -#: bookwyrm/templates/moderation/reports.html:13 -#, fuzzy, python-format -#| msgid "Lists: %(username)s" +#: bookwyrm/templates/moderation/reports.html:14 +#, python-format msgid "Reports: %(server_name)s" -msgstr "Listas: %(username)s" +msgstr "Informes: %(server_name)s" -#: bookwyrm/templates/moderation/reports.html:27 -#, fuzzy -#| msgid "Shelved" +#: bookwyrm/templates/moderation/reports.html:28 msgid "Resolved" -msgstr "Archivado" +msgstr "Resuelto" -#: bookwyrm/templates/moderation/reports.html:34 -#, fuzzy -#| msgid "No books found" +#: bookwyrm/templates/moderation/reports.html:37 msgid "No reports found." -msgstr "No se encontró ningún libro" +msgstr "No se encontró ningún informe." #: bookwyrm/templates/notifications.html:14 msgid "Delete notifications" @@ -1532,18 +1567,29 @@ msgstr "Borrar notificaciones" #: bookwyrm/templates/notifications.html:53 #, python-format -msgid "favorited your review of %(book_title)s" -msgstr "le gustó tu reseña de %(book_title)s" +msgid "" +"favorited your review of %(book_title)s" +msgstr "" +"le gustó tu reseña de %(book_title)s" #: bookwyrm/templates/notifications.html:55 #, python-format -msgid "favorited your comment on %(book_title)s" -msgstr "le gustó tu comentario en %(book_title)s" +msgid "" +"favorited your comment on %(book_title)s" +msgstr "" +"le gustó tu comentario en %(book_title)s" #: bookwyrm/templates/notifications.html:57 #, python-format -msgid "favorited your quote from %(book_title)s" -msgstr "le gustó tu cita de %(book_title)s" +msgid "" +"favorited your quote from %(book_title)s" +msgstr "" +"le gustó tu cita de %(book_title)s" #: bookwyrm/templates/notifications.html:59 #, python-format @@ -1552,18 +1598,30 @@ msgstr "le gustó tu status" #: bookwyrm/templates/notifications.html:64 #, python-format -msgid "mentioned you in a review of %(book_title)s" -msgstr "te mencionó en una reseña de %(book_title)s" +msgid "" +"mentioned you in a review of " +"%(book_title)s" +msgstr "" +"te mencionó en una reseña de " +"%(book_title)s" #: bookwyrm/templates/notifications.html:66 #, python-format -msgid "mentioned you in a comment on %(book_title)s" -msgstr "te mencionó en un comentario de %(book_title)s" +msgid "" +"mentioned you in a comment on " +"%(book_title)s" +msgstr "" +"te mencionó en un comentario de " +"%(book_title)s" #: bookwyrm/templates/notifications.html:68 #, python-format -msgid "mentioned you in a quote from %(book_title)s" -msgstr "te mencionó en una cita de %(book_title)s" +msgid "" +"mentioned you in a quote from " +"%(book_title)s" +msgstr "" +"te mencionó en una cita de %(book_title)s" #: bookwyrm/templates/notifications.html:70 #, python-format @@ -1572,23 +1630,39 @@ msgstr "te mencionó en un status" #: bookwyrm/templates/notifications.html:75 #, python-format -msgid "replied to your review of %(book_title)s" -msgstr "respondió a tu reseña de %(book_title)s" +msgid "" +"replied to your review of %(book_title)s" +msgstr "" +"respondió a tu reseña de %(book_title)s" #: bookwyrm/templates/notifications.html:77 #, python-format -msgid "replied to your comment on %(book_title)s" -msgstr "respondió a tu comentario en %(book_title)s" +msgid "" +"replied to your comment on %(book_title)s" +msgstr "" +"respondió a tu comentario en %(book_title)s" #: bookwyrm/templates/notifications.html:79 #, python-format -msgid "replied to your quote from %(book_title)s" -msgstr "respondió a tu cita de %(book_title)s" +msgid "" +"replied to your quote from %(book_title)s" +msgstr "" +"respondió a tu cita de %(book_title)s" #: bookwyrm/templates/notifications.html:81 #, python-format -msgid "replied to your status" -msgstr "respondió a tu status" +msgid "" +"replied to your status" +msgstr "" +"respondió a tu status" #: bookwyrm/templates/notifications.html:85 msgid "followed you" @@ -1600,18 +1674,29 @@ msgstr "te quiere seguir" #: bookwyrm/templates/notifications.html:94 #, python-format -msgid "boosted your review of %(book_title)s" -msgstr "respaldó tu reseña de %(book_title)s" +msgid "" +"boosted your review of %(book_title)s" +msgstr "" +"respaldó tu reseña de %(book_title)s" #: bookwyrm/templates/notifications.html:96 #, python-format -msgid "boosted your comment on%(book_title)s" -msgstr "respaldó tu comentario en%(book_title)s" +msgid "" +"boosted your comment on%(book_title)s" +msgstr "" +"respaldó tu comentario en%(book_title)s" #: bookwyrm/templates/notifications.html:98 #, python-format -msgid "boosted your quote from %(book_title)s" -msgstr "respaldó tucita de %(book_title)s" +msgid "" +"boosted your quote from %(book_title)s" +msgstr "" +"respaldó tucita de %(book_title)s" #: bookwyrm/templates/notifications.html:100 #, python-format @@ -1620,13 +1705,21 @@ msgstr "respaldó tu status" #: bookwyrm/templates/notifications.html:104 #, python-format -msgid " added %(book_title)s to your list \"%(list_name)s\"" -msgstr " agregó %(book_title)s a tu lista \"%(list_name)s\"" +msgid "" +" added %(book_title)s to your list " +"\"%(list_name)s\"" +msgstr "" +" agregó %(book_title)s a tu lista " +"\"%(list_name)s\"" #: bookwyrm/templates/notifications.html:106 #, python-format -msgid " suggested adding %(book_title)s to your list \"%(list_name)s\"" -msgstr " sugirió agregar %(book_title)s a tu lista \"%(list_name)s\"" +msgid "" +" suggested adding %(book_title)s to " +"your list \"%(list_name)s\"" +msgstr "" +" sugirió agregar %(book_title)s a tu " +"lista \"%(list_name)s\"" #: bookwyrm/templates/notifications.html:110 #, python-format @@ -1636,7 +1729,7 @@ msgstr "Tu importación ha terminado." #: bookwyrm/templates/notifications.html:113 #, python-format msgid "A new report needs moderation." -msgstr "" +msgstr "Un informe nuevo se requiere moderación." #: bookwyrm/templates/notifications.html:139 msgid "You're all caught up!" @@ -1649,7 +1742,9 @@ msgstr "Confirmar contraseña:" #: bookwyrm/templates/password_reset_request.html:14 msgid "A link to reset your password will be sent to your email address" -msgstr "Un enlace para restablecer tu contraseña se enviará a tu dirección de correo electrónico" +msgstr "" +"Un enlace para restablecer tu contraseña se enviará a tu dirección de correo " +"electrónico" #: bookwyrm/templates/password_reset_request.html:28 msgid "Reset password" @@ -1683,12 +1778,16 @@ msgstr "Editar perfil" #: bookwyrm/templates/preferences/edit_user.html:46 msgid "Show set reading goal prompt in feed:" -msgstr "" +msgstr "Mostrar meta de lectura en el feed:" #: bookwyrm/templates/preferences/edit_user.html:62 #, python-format -msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users." +msgid "" +"Your account will show up in the directory, and may " +"be recommended to other BookWyrm users." msgstr "" +"Tu cuenta se aparecerá en el directorio, y puede " +"ser recomendado a otros usuarios de BookWyrm." #: bookwyrm/templates/preferences/edit_user.html:65 msgid "Preferred Timezone: " @@ -1698,6 +1797,10 @@ msgstr "Huso horario preferido" msgid "Account" msgstr "Cuenta" +#: bookwyrm/templates/preferences/preferences_layout.html:14 +msgid "Profile" +msgstr "Perfil" + #: bookwyrm/templates/preferences/preferences_layout.html:20 msgid "Relationships" msgstr "Relaciones" @@ -1727,135 +1830,187 @@ msgstr "No se encontró ningúna lista correspondiente a \"%(query)s\"" msgid "Administration" msgstr "Adminstración" -#: bookwyrm/templates/settings/admin_layout.html:15 +#: bookwyrm/templates/settings/admin_layout.html:22 msgid "Manage Users" msgstr "Administrar usuarios" -#: bookwyrm/templates/settings/admin_layout.html:19 -#: bookwyrm/templates/settings/user_admin.html:3 -#: bookwyrm/templates/settings/user_admin.html:10 +#: bookwyrm/templates/settings/admin_layout.html:26 +#: bookwyrm/templates/user_admin/user_admin.html:3 +#: bookwyrm/templates/user_admin/user_admin.html:10 msgid "Users" -msgstr "" +msgstr "Usuarios" -#: bookwyrm/templates/settings/admin_layout.html:32 +#: bookwyrm/templates/settings/admin_layout.html:39 #: bookwyrm/templates/settings/federation.html:3 #: bookwyrm/templates/settings/federation.html:5 msgid "Federated Servers" msgstr "Servidores federalizados" -#: bookwyrm/templates/settings/admin_layout.html:37 +#: bookwyrm/templates/settings/admin_layout.html:44 msgid "Instance Settings" msgstr "Configuración de instancia" -#: bookwyrm/templates/settings/admin_layout.html:41 +#: bookwyrm/templates/settings/admin_layout.html:48 #: bookwyrm/templates/settings/site.html:4 #: bookwyrm/templates/settings/site.html:6 msgid "Site Settings" msgstr "Configuración de sitio" -#: bookwyrm/templates/settings/admin_layout.html:44 +#: bookwyrm/templates/settings/admin_layout.html:51 #: bookwyrm/templates/settings/site.html:13 msgid "Instance Info" msgstr "Información de instancia" -#: bookwyrm/templates/settings/admin_layout.html:45 +#: bookwyrm/templates/settings/admin_layout.html:52 #: bookwyrm/templates/settings/site.html:39 msgid "Images" msgstr "Imagenes" -#: bookwyrm/templates/settings/admin_layout.html:46 +#: bookwyrm/templates/settings/admin_layout.html:53 #: bookwyrm/templates/settings/site.html:59 msgid "Footer Content" msgstr "Contenido del pie de página" -#: bookwyrm/templates/settings/admin_layout.html:47 +#: bookwyrm/templates/settings/admin_layout.html:54 #: bookwyrm/templates/settings/site.html:77 msgid "Registration" msgstr "Registración" -#: bookwyrm/templates/settings/federated_server.html:7 -msgid "Back to server list" -msgstr "" +#: bookwyrm/templates/settings/edit_server.html:3 +#: bookwyrm/templates/settings/edit_server.html:6 +#: bookwyrm/templates/settings/edit_server.html:20 +#: bookwyrm/templates/settings/federation.html:9 +#: bookwyrm/templates/settings/federation.html:10 +#: bookwyrm/templates/settings/server_blocklist.html:3 +#: bookwyrm/templates/settings/server_blocklist.html:20 +msgid "Add server" +msgstr "Agregar servidor" +#: bookwyrm/templates/settings/edit_server.html:7 #: bookwyrm/templates/settings/federated_server.html:12 -msgid "Details" -msgstr "" +#: bookwyrm/templates/settings/server_blocklist.html:7 +msgid "Back to server list" +msgstr "Volver a la lista de servidores" -#: bookwyrm/templates/settings/federated_server.html:15 -#, fuzzy -#| msgid "Software" -msgid "Software:" -msgstr "Software" +#: bookwyrm/templates/settings/edit_server.html:16 +#: bookwyrm/templates/settings/server_blocklist.html:16 +msgid "Import block list" +msgstr "Importar lista de bloqueo" -#: bookwyrm/templates/settings/federated_server.html:19 -#, fuzzy -#| msgid "Description:" -msgid "Version:" -msgstr "Descripción:" +#: bookwyrm/templates/settings/edit_server.html:30 +msgid "Instance:" +msgstr "Instancia:" -#: bookwyrm/templates/settings/federated_server.html:23 -#, fuzzy -#| msgid "Status" +#: bookwyrm/templates/settings/edit_server.html:37 +#: bookwyrm/templates/settings/federated_server.html:29 +#: bookwyrm/templates/user_admin/user_info.html:34 msgid "Status:" -msgstr "Status" +msgstr "Status:" -#: bookwyrm/templates/settings/federated_server.html:30 +#: bookwyrm/templates/settings/edit_server.html:41 +#: bookwyrm/templates/settings/federated_server.html:9 +msgid "Blocked" +msgstr "Bloqueado" + +#: bookwyrm/templates/settings/edit_server.html:48 +#: bookwyrm/templates/settings/federated_server.html:21 +#: bookwyrm/templates/user_admin/user_info.html:26 +msgid "Software:" +msgstr "Software:" + +#: bookwyrm/templates/settings/edit_server.html:55 +#: bookwyrm/templates/settings/federated_server.html:25 +#: bookwyrm/templates/user_admin/user_info.html:30 +msgid "Version:" +msgstr "Versión:" + +#: bookwyrm/templates/settings/edit_server.html:64 +msgid "Notes:" +msgstr "Notas:" + +#: bookwyrm/templates/settings/federated_server.html:18 +msgid "Details" +msgstr "Detalles" + +#: bookwyrm/templates/settings/federated_server.html:36 #: bookwyrm/templates/user/user_layout.html:50 msgid "Activity" msgstr "Actividad" -#: bookwyrm/templates/settings/federated_server.html:33 -#, fuzzy -#| msgid "Username:" +#: bookwyrm/templates/settings/federated_server.html:39 msgid "Users:" -msgstr "Nombre de usuario:" +msgstr "Usuarios:" -#: bookwyrm/templates/settings/federated_server.html:36 -#: bookwyrm/templates/settings/federated_server.html:43 +#: bookwyrm/templates/settings/federated_server.html:42 +#: bookwyrm/templates/settings/federated_server.html:49 msgid "View all" -msgstr "" +msgstr "Ver todos" -#: bookwyrm/templates/settings/federated_server.html:40 -#, fuzzy -#| msgid "Recent Imports" +#: bookwyrm/templates/settings/federated_server.html:46 msgid "Reports:" -msgstr "Importaciones recientes" - -#: bookwyrm/templates/settings/federated_server.html:47 -#, fuzzy -#| msgid "followed you" -msgid "Followed by us:" -msgstr "te siguió" +msgstr "Informes:" #: bookwyrm/templates/settings/federated_server.html:53 -#, fuzzy -#| msgid "followed you" -msgid "Followed by them:" -msgstr "te siguió" +msgid "Followed by us:" +msgstr "Seguido por nosotros:" #: bookwyrm/templates/settings/federated_server.html:59 -#, fuzzy -#| msgid "Blocked Users" -msgid "Blocked by us:" -msgstr "Usuarios bloqueados" +msgid "Followed by them:" +msgstr "Seguido por ellos:" -#: bookwyrm/templates/settings/federation.html:13 +#: bookwyrm/templates/settings/federated_server.html:65 +msgid "Blocked by us:" +msgstr "Bloqueado por nosotros:" + +#: bookwyrm/templates/settings/federated_server.html:77 +#: bookwyrm/templates/user_admin/user_info.html:39 +msgid "Notes" +msgstr "Notas" + +#: bookwyrm/templates/settings/federated_server.html:80 +msgid "Edit" +msgstr "Editar" + +#: bookwyrm/templates/settings/federated_server.html:100 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:3 +msgid "Actions" +msgstr "Acciones" + +#: bookwyrm/templates/settings/federated_server.html:104 +#: bookwyrm/templates/snippets/block_button.html:5 +msgid "Block" +msgstr "Bloquear" + +#: bookwyrm/templates/settings/federated_server.html:105 +msgid "All users from this instance will be deactivated." +msgstr "Todos los usuarios en esta instancia serán desactivados." + +#: bookwyrm/templates/settings/federated_server.html:110 +#: bookwyrm/templates/snippets/block_button.html:10 +msgid "Un-block" +msgstr "Desbloquear" + +#: bookwyrm/templates/settings/federated_server.html:111 +msgid "All users from this instance will be re-activated." +msgstr "Todos los usuarios en esta instancia serán re-activados." + +#: bookwyrm/templates/settings/federation.html:20 +#: bookwyrm/templates/user_admin/server_filter.html:5 msgid "Server name" msgstr "Nombre de servidor" -#: bookwyrm/templates/settings/federation.html:17 -#, fuzzy -#| msgid "Federated" +#: bookwyrm/templates/settings/federation.html:24 msgid "Date federated" -msgstr "Federalizado" +msgstr "Fecha de federalización" -#: bookwyrm/templates/settings/federation.html:21 +#: bookwyrm/templates/settings/federation.html:28 msgid "Software" msgstr "Software" -#: bookwyrm/templates/settings/federation.html:24 -#: bookwyrm/templates/settings/manage_invite_requests.html:33 -#: bookwyrm/templates/settings/user_admin.html:32 +#: bookwyrm/templates/settings/federation.html:31 +#: bookwyrm/templates/settings/manage_invite_requests.html:44 +#: bookwyrm/templates/settings/status_filter.html:5 +#: bookwyrm/templates/user_admin/user_admin.html:34 msgid "Status" msgstr "Status" @@ -1863,72 +2018,71 @@ msgstr "Status" #: bookwyrm/templates/settings/manage_invite_requests.html:11 #: bookwyrm/templates/settings/manage_invite_requests.html:25 #: bookwyrm/templates/settings/manage_invites.html:11 -#, fuzzy -#| msgid "Invites" msgid "Invite Requests" -msgstr "Invitaciones" +msgstr "Solicitudes de invitación" #: bookwyrm/templates/settings/manage_invite_requests.html:23 msgid "Ignored Invite Requests" -msgstr "" +msgstr "Solicitudes de invitación ignoradas" -#: bookwyrm/templates/settings/manage_invite_requests.html:31 -msgid "Date" -msgstr "" +#: bookwyrm/templates/settings/manage_invite_requests.html:35 +msgid "Date requested" +msgstr "Fecha solicitada" -#: bookwyrm/templates/settings/manage_invite_requests.html:32 +#: bookwyrm/templates/settings/manage_invite_requests.html:39 +msgid "Date accepted" +msgstr "Fecha de aceptación" + +#: bookwyrm/templates/settings/manage_invite_requests.html:42 msgid "Email" -msgstr "" - -#: bookwyrm/templates/settings/manage_invite_requests.html:34 -#, fuzzy -#| msgid "Notifications" -msgid "Action" -msgstr "Notificaciones" - -#: bookwyrm/templates/settings/manage_invite_requests.html:37 -#, fuzzy -#| msgid "Follow Requests" -msgid "No requests" -msgstr "Solicitudes de seguidor" - -#: bookwyrm/templates/settings/manage_invite_requests.html:45 -#, fuzzy -#| msgid "Accept" -msgid "Accepted" -msgstr "Aceptar" +msgstr "Correo electronico" #: bookwyrm/templates/settings/manage_invite_requests.html:47 -msgid "Sent" -msgstr "" +msgid "Action" +msgstr "Acción" -#: bookwyrm/templates/settings/manage_invite_requests.html:49 -msgid "Requested" -msgstr "" - -#: bookwyrm/templates/settings/manage_invite_requests.html:57 -msgid "Send invite" -msgstr "" +#: bookwyrm/templates/settings/manage_invite_requests.html:50 +msgid "No requests" +msgstr "No solicitudes" #: bookwyrm/templates/settings/manage_invite_requests.html:59 +#: bookwyrm/templates/settings/status_filter.html:16 +msgid "Accepted" +msgstr "Aceptado" + +#: bookwyrm/templates/settings/manage_invite_requests.html:61 +#: bookwyrm/templates/settings/status_filter.html:12 +msgid "Sent" +msgstr "Enviado" + +#: bookwyrm/templates/settings/manage_invite_requests.html:63 +#: bookwyrm/templates/settings/status_filter.html:8 +msgid "Requested" +msgstr "Solicitado" + +#: bookwyrm/templates/settings/manage_invite_requests.html:73 +msgid "Send invite" +msgstr "Enviar invitación" + +#: bookwyrm/templates/settings/manage_invite_requests.html:75 msgid "Re-send invite" -msgstr "" +msgstr "Re-enviar invitación" -#: bookwyrm/templates/settings/manage_invite_requests.html:70 +#: bookwyrm/templates/settings/manage_invite_requests.html:95 msgid "Ignore" -msgstr "" +msgstr "Ignorar" -#: bookwyrm/templates/settings/manage_invite_requests.html:72 -msgid "Un-gnore" -msgstr "" +#: bookwyrm/templates/settings/manage_invite_requests.html:97 +msgid "Un-ignore" +msgstr "Des-ignorar" -#: bookwyrm/templates/settings/manage_invite_requests.html:83 +#: bookwyrm/templates/settings/manage_invite_requests.html:108 msgid "Back to pending requests" -msgstr "" +msgstr "Volver a las solicitudes pendientes" -#: bookwyrm/templates/settings/manage_invite_requests.html:85 +#: bookwyrm/templates/settings/manage_invite_requests.html:110 msgid "View ignored requests" -msgstr "" +msgstr "Ver solicitudes ignoradas" #: bookwyrm/templates/settings/manage_invites.html:21 msgid "Generate New Invite" @@ -1966,6 +2120,23 @@ msgstr "Número de usos" msgid "No active invites" msgstr "No invitaciónes activas" +#: bookwyrm/templates/settings/server_blocklist.html:6 +msgid "Import Blocklist" +msgstr "Importar lista de bloqueo" + +#: bookwyrm/templates/settings/server_blocklist.html:26 +#: bookwyrm/templates/snippets/goal_progress.html:5 +msgid "Success!" +msgstr "¡Meta logrado!" + +#: bookwyrm/templates/settings/server_blocklist.html:30 +msgid "Successfully blocked:" +msgstr "Se bloqueó exitosamente:" + +#: bookwyrm/templates/settings/server_blocklist.html:32 +msgid "Failed:" +msgstr "Falló:" + #: bookwyrm/templates/settings/site.html:15 msgid "Instance Name:" msgstr "Nombre de instancia:" @@ -2015,161 +2186,107 @@ msgid "Allow registration:" msgstr "Permitir registración:" #: bookwyrm/templates/settings/site.html:83 -#, fuzzy -#| msgid "Follow Requests" msgid "Allow invite requests:" -msgstr "Solicitudes de seguidor" +msgstr "Permitir solicitudes de invitación:" #: bookwyrm/templates/settings/site.html:87 msgid "Registration closed text:" msgstr "Texto de registración cerrada:" -#: bookwyrm/templates/settings/user_admin.html:7 -#, python-format -msgid "Users: %(server_name)s" -msgstr "" +#: bookwyrm/templates/snippets/book_cover.html:20 +#: bookwyrm/templates/snippets/search_result_text.html:10 +msgid "No cover" +msgstr "Sin portada" -#: bookwyrm/templates/settings/user_admin.html:20 -#, fuzzy -#| msgid "Username:" -msgid "Username" -msgstr "Nombre de usuario:" - -#: bookwyrm/templates/settings/user_admin.html:24 -#, fuzzy -#| msgid "Added:" -msgid "Date Added" -msgstr "Agregado:" - -#: bookwyrm/templates/settings/user_admin.html:28 -msgid "Last Active" -msgstr "" - -#: bookwyrm/templates/settings/user_admin.html:36 -#, fuzzy -#| msgid "Remove" -msgid "Remote server" -msgstr "Quitar" - -#: bookwyrm/templates/settings/user_admin.html:45 -#, fuzzy -#| msgid "Activity" -msgid "Active" -msgstr "Actividad" - -#: bookwyrm/templates/settings/user_admin.html:45 -msgid "Inactive" -msgstr "" - -#: bookwyrm/templates/settings/user_admin.html:50 -msgid "Not set" -msgstr "" - -#: bookwyrm/templates/snippets/block_button.html:5 -msgid "Block" -msgstr "Bloquear" - -#: bookwyrm/templates/snippets/block_button.html:10 -msgid "Un-block" -msgstr "Desbloquear" - -#: bookwyrm/templates/snippets/book_titleby.html:3 +#: bookwyrm/templates/snippets/book_titleby.html:4 #, python-format msgid "%(title)s by " msgstr "%(title)s por " #: bookwyrm/templates/snippets/boost_button.html:8 #: bookwyrm/templates/snippets/boost_button.html:9 -#: bookwyrm/templates/snippets/status/status_body.html:51 -#: bookwyrm/templates/snippets/status/status_body.html:52 -msgid "Boost status" -msgstr "Status de respaldo" +#, fuzzy +#| msgid "boosted" +msgid "Boost" +msgstr "respaldó" +#: bookwyrm/templates/snippets/boost_button.html:15 #: bookwyrm/templates/snippets/boost_button.html:16 -#: bookwyrm/templates/snippets/boost_button.html:17 -msgid "Un-boost status" +#, fuzzy +#| msgid "Un-boost status" +msgid "Un-boost" msgstr "Status de des-respaldo" #: bookwyrm/templates/snippets/content_warning_field.html:3 msgid "Spoiler alert:" msgstr "Alerta de spoiler:" -#: bookwyrm/templates/snippets/content_warning_field.html:4 +#: bookwyrm/templates/snippets/content_warning_field.html:10 msgid "Spoilers ahead!" msgstr "¡Advertencia, ya vienen spoilers!" -#: bookwyrm/templates/snippets/create_status.html:9 +#: bookwyrm/templates/snippets/create_status.html:17 msgid "Review" msgstr "Reseña" -#: bookwyrm/templates/snippets/create_status.html:15 +#: bookwyrm/templates/snippets/create_status.html:39 msgid "Quote" msgstr "Cita" -#: bookwyrm/templates/snippets/create_status_form.html:18 -#, fuzzy -#| msgid "Comment" -msgid "Comment:" -msgstr "Comentario" - #: bookwyrm/templates/snippets/create_status_form.html:20 -#, fuzzy -#| msgid "Quote" -msgid "Quote:" -msgstr "Cita" +msgid "Comment:" +msgstr "Comentario:" #: bookwyrm/templates/snippets/create_status_form.html:22 -#, fuzzy -#| msgid "Review" +msgid "Quote:" +msgstr "Cita:" + +#: bookwyrm/templates/snippets/create_status_form.html:24 msgid "Review:" -msgstr "Reseña" +msgstr "Reseña:" -#: bookwyrm/templates/snippets/create_status_form.html:29 -#: bookwyrm/templates/user/shelf.html:81 -msgid "Rating" -msgstr "Calificación" +#: bookwyrm/templates/snippets/create_status_form.html:42 +#: bookwyrm/templates/snippets/status/layout.html:30 +#: bookwyrm/templates/snippets/status/layout.html:48 +#: bookwyrm/templates/snippets/status/layout.html:49 +msgid "Reply" +msgstr "Respuesta" -#: bookwyrm/templates/snippets/create_status_form.html:31 -#: bookwyrm/templates/snippets/rate_action.html:14 -#: bookwyrm/templates/snippets/stars.html:3 -msgid "No rating" -msgstr "No calificación" - -#: bookwyrm/templates/snippets/create_status_form.html:64 +#: bookwyrm/templates/snippets/create_status_form.html:67 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:16 msgid "Progress:" msgstr "Progreso:" -#: bookwyrm/templates/snippets/create_status_form.html:71 +#: bookwyrm/templates/snippets/create_status_form.html:75 #: bookwyrm/templates/snippets/readthrough_form.html:22 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:30 msgid "pages" msgstr "páginas" -#: bookwyrm/templates/snippets/create_status_form.html:72 +#: bookwyrm/templates/snippets/create_status_form.html:76 #: bookwyrm/templates/snippets/readthrough_form.html:23 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:31 msgid "percent" msgstr "por ciento" -#: bookwyrm/templates/snippets/create_status_form.html:77 +#: bookwyrm/templates/snippets/create_status_form.html:82 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:36 #, python-format msgid "of %(pages)s pages" msgstr "de %(pages)s páginas" -#: bookwyrm/templates/snippets/create_status_form.html:89 +#: bookwyrm/templates/snippets/create_status_form.html:97 msgid "Include spoiler alert" msgstr "Incluir alerta de spoiler" -#: bookwyrm/templates/snippets/create_status_form.html:95 +#: bookwyrm/templates/snippets/create_status_form.html:104 #: bookwyrm/templates/snippets/privacy-icons.html:15 #: bookwyrm/templates/snippets/privacy-icons.html:16 #: bookwyrm/templates/snippets/privacy_select.html:19 msgid "Private" msgstr "Privada" -#: bookwyrm/templates/snippets/create_status_form.html:102 +#: bookwyrm/templates/snippets/create_status_form.html:115 msgid "Post" msgstr "Compartir" @@ -2179,55 +2296,54 @@ msgstr "¿Eliminar estas fechas de lectura?" #: bookwyrm/templates/snippets/delete_readthrough_modal.html:7 #, python-format -msgid "You are deleting this readthrough and its %(count)s associated progress updates." -msgstr "Estás eliminando esta lectura y sus %(count)s actualizaciones de progreso asociados." +msgid "" +"You are deleting this readthrough and its %(count)s associated progress " +"updates." +msgstr "" +"Estás eliminando esta lectura y sus %(count)s actualizaciones de progreso " +"asociados." #: bookwyrm/templates/snippets/delete_readthrough_modal.html:15 #: bookwyrm/templates/snippets/follow_request_buttons.html:13 +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:391 msgid "Delete" msgstr "Eliminar" #: bookwyrm/templates/snippets/fav_button.html:7 -#: bookwyrm/templates/snippets/fav_button.html:8 -#: bookwyrm/templates/snippets/status/status_body.html:55 -#: bookwyrm/templates/snippets/status/status_body.html:56 -msgid "Like status" -msgstr "Me gusta status" +#: bookwyrm/templates/snippets/fav_button.html:9 +msgid "Like" +msgstr "" #: bookwyrm/templates/snippets/fav_button.html:15 #: bookwyrm/templates/snippets/fav_button.html:16 -msgid "Un-like status" +#, fuzzy +#| msgid "Un-like status" +msgid "Un-like" msgstr "Quitar me gusta de status" #: bookwyrm/templates/snippets/filters_panel/filters_panel.html:7 -#, fuzzy -#| msgid "Show less" msgid "Show filters" -msgstr "Mostrar menos" +msgstr "Mostrar filtros" #: bookwyrm/templates/snippets/filters_panel/filters_panel.html:9 msgid "Hide filters" -msgstr "" +msgstr "Ocultar filtros" -#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:19 +#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:22 msgid "Apply filters" -msgstr "" +msgstr "Aplicar filtros" -#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:23 -#, fuzzy -#| msgid "Clear search" +#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:26 msgid "Clear filters" -msgstr "Borrar búsqueda" +msgstr "Borrar filtros" #: bookwyrm/templates/snippets/follow_button.html:12 msgid "Follow" msgstr "Seguir" #: bookwyrm/templates/snippets/follow_button.html:18 -#, fuzzy -#| msgid "Send follow request" msgid "Undo follow request" -msgstr "Envia solicitud de seguidor" +msgstr "Des-enviar solicitud de seguidor" #: bookwyrm/templates/snippets/follow_button.html:20 msgid "Unfollow" @@ -2237,6 +2353,11 @@ msgstr "Dejar de seguir" msgid "Accept" msgstr "Aceptar" +#: bookwyrm/templates/snippets/form_rate_stars.html:20 +#: bookwyrm/templates/snippets/stars.html:13 +msgid "No rating" +msgstr "No calificación" + #: bookwyrm/templates/snippets/generated_status/goal.html:1 #, python-format msgid "set a goal to read %(counter)s book in %(year)s" @@ -2245,29 +2366,42 @@ msgstr[0] "estableció una meta de leer %(counter)s libro en %(year)s" msgstr[1] "estableció una meta de leer %(counter)s libros en %(year)s" #: bookwyrm/templates/snippets/generated_status/rating.html:3 -#, fuzzy, python-format -#| msgid "%(title)s by " -msgid "Rated %(title)s: %(display_rating)s star" -msgid_plural "Rated %(title)s: %(display_rating)s stars" -msgstr[0] "%(title)s por " -msgstr[1] "%(title)s por " +#, python-format +msgid "" +"Rated %(title)s: %(display_rating)s star" +msgid_plural "" +"Rated %(title)s: %(display_rating)s stars" +msgstr[0] "" +"Reseño %(title)s: %(display_rating)s " +"estrella" +msgstr[1] "" +"Reseño %(title)s: %(display_rating)s " +"estrellas" #: bookwyrm/templates/snippets/generated_status/review_pure_name.html:4 #, python-format -msgid "Review of \"%(book_title)s\" (%(display_rating)s star): %(review_title)s" -msgid_plural "Review of \"%(book_title)s\" (%(display_rating)s stars): %(review_title)s" +msgid "" +"Review of \"%(book_title)s\" (%(display_rating)s star): %(review_title)s" +msgid_plural "" +"Review of \"%(book_title)s\" (%(display_rating)s stars): %(review_title)s" msgstr[0] "" +"Reseña de \"%(book_title)s\" (%(display_rating)s estrella): %(review_title)s" msgstr[1] "" +"Reseña de \"%(book_title)s\" (%(display_rating)s estrellas): %(review_title)s" #: bookwyrm/templates/snippets/generated_status/review_pure_name.html:8 #, python-format msgid "Review of \"%(book_title)s\": %(review_title)s" -msgstr "" +msgstr "Reseña de \"%(book_title)s\": %(review_title)s" #: bookwyrm/templates/snippets/goal_card.html:23 #, python-format -msgid "You can set or change your reading goal any time from your profile page" -msgstr "Puedes establecer o cambiar tu meta de lectura en cualquier momento que desees desde tu perfil" +msgid "" +"You can set or change your reading goal any time from your profile page" +msgstr "" +"Puedes establecer o cambiar tu meta de lectura en cualquier momento que " +"desees desde tu perfil" #: bookwyrm/templates/snippets/goal_form.html:9 msgid "Reading goal:" @@ -2292,10 +2426,6 @@ msgstr "Compartir con tu feed" msgid "Set goal" msgstr "Establecer meta" -#: bookwyrm/templates/snippets/goal_progress.html:5 -msgid "Success!" -msgstr "¡Meta logrado!" - #: bookwyrm/templates/snippets/goal_progress.html:7 #, python-format msgid "%(percent)s%% complete!" @@ -2303,19 +2433,36 @@ msgstr "%(percent)s%% terminado!" #: bookwyrm/templates/snippets/goal_progress.html:10 #, python-format -msgid "You've read %(read_count)s of %(goal_count)s books." -msgstr "Has leído %(read_count)s de %(goal_count)s libros." +msgid "" +"You've read %(read_count)s of %(goal_count)s books." +msgstr "" +"Has leído %(read_count)s de %(goal_count)s libros." #: bookwyrm/templates/snippets/goal_progress.html:12 #, python-format -msgid "%(username)s has read %(read_count)s of %(goal_count)s books." -msgstr "%(username)s ha leído %(read_count)s de %(goal_count)s libros." +msgid "" +"%(username)s has read %(read_count)s of %(goal_count)s " +"books." +msgstr "" +"%(username)s ha leído %(read_count)s de %(goal_count)s " +"libros." -#: bookwyrm/templates/snippets/pagination.html:7 +#: bookwyrm/templates/snippets/page_text.html:4 +#, python-format +msgid "page %(page)s of %(total_pages)s" +msgstr "página %(page)s de %(total_pages)s" + +#: bookwyrm/templates/snippets/page_text.html:6 +#, fuzzy, python-format +#| msgid "page %(page)s" +msgid "page %(page)s" +msgstr "página %(pages)s" + +#: bookwyrm/templates/snippets/pagination.html:12 msgid "Previous" msgstr "Anterior" -#: bookwyrm/templates/snippets/pagination.html:15 +#: bookwyrm/templates/snippets/pagination.html:23 msgid "Next" msgstr "Siguiente" @@ -2348,7 +2495,7 @@ msgstr "Seguidores" msgid "Leave a rating" msgstr "Da una calificación" -#: bookwyrm/templates/snippets/rate_action.html:29 +#: bookwyrm/templates/snippets/rate_action.html:19 msgid "Rate" msgstr "Calificar" @@ -2356,28 +2503,28 @@ msgstr "Calificar" msgid "Progress Updates:" msgstr "Actualizaciones de progreso:" -#: bookwyrm/templates/snippets/readthrough.html:12 +#: bookwyrm/templates/snippets/readthrough.html:14 msgid "finished" msgstr "terminado" -#: bookwyrm/templates/snippets/readthrough.html:15 +#: bookwyrm/templates/snippets/readthrough.html:25 msgid "Show all updates" msgstr "Mostrar todas las actualizaciones" -#: bookwyrm/templates/snippets/readthrough.html:31 +#: bookwyrm/templates/snippets/readthrough.html:41 msgid "Delete this progress update" msgstr "Eliminar esta actualización de progreso" -#: bookwyrm/templates/snippets/readthrough.html:41 +#: bookwyrm/templates/snippets/readthrough.html:51 msgid "started" msgstr "empezado" -#: bookwyrm/templates/snippets/readthrough.html:47 -#: bookwyrm/templates/snippets/readthrough.html:61 +#: bookwyrm/templates/snippets/readthrough.html:57 +#: bookwyrm/templates/snippets/readthrough.html:71 msgid "Edit read dates" msgstr "Editar fechas de lectura" -#: bookwyrm/templates/snippets/readthrough.html:51 +#: bookwyrm/templates/snippets/readthrough.html:61 msgid "Delete these read dates" msgstr "Eliminar estas fechas de lectura" @@ -2401,37 +2548,29 @@ msgid "Sign Up" msgstr "Inscribirse" #: bookwyrm/templates/snippets/report_button.html:5 -#, fuzzy -#| msgid "Import" msgid "Report" -msgstr "Importar" +msgstr "Reportar" #: bookwyrm/templates/snippets/rss_title.html:5 -#: bookwyrm/templates/snippets/status/status_header.html:11 +#: bookwyrm/templates/snippets/status/status_header.html:34 msgid "rated" msgstr "calificó" #: bookwyrm/templates/snippets/rss_title.html:7 -#: bookwyrm/templates/snippets/status/status_header.html:13 +#: bookwyrm/templates/snippets/status/status_header.html:36 msgid "reviewed" msgstr "reseñó" #: bookwyrm/templates/snippets/rss_title.html:9 -#: bookwyrm/templates/snippets/status/status_header.html:15 +#: bookwyrm/templates/snippets/status/status_header.html:38 msgid "commented on" msgstr "comentó en" #: bookwyrm/templates/snippets/rss_title.html:11 -#: bookwyrm/templates/snippets/status/status_header.html:17 +#: bookwyrm/templates/snippets/status/status_header.html:40 msgid "quoted" msgstr "citó" -#: bookwyrm/templates/snippets/search_result_text.html:10 -#, fuzzy -#| msgid "Add cover" -msgid "No cover" -msgstr "Agregar portada" - #: bookwyrm/templates/snippets/search_result_text.html:22 #, python-format msgid "by %(author)s" @@ -2442,10 +2581,8 @@ msgid "Import book" msgstr "Importar libro" #: bookwyrm/templates/snippets/shelf_selector.html:4 -#, fuzzy -#| msgid "Your books" msgid "Move book" -msgstr "Tus libros" +msgstr "Mover libro" #: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:5 #, python-format @@ -2453,11 +2590,9 @@ msgid "Finish \"%(book_title)s\"" msgstr "Terminar \"%(book_title)s\"" #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:5 -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:35 -#, fuzzy -#| msgid "Updates" +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:34 msgid "Update progress" -msgstr "Actualizaciones" +msgstr "Progreso de actualización" #: bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown.html:5 msgid "More shelves" @@ -2476,11 +2611,10 @@ msgstr "Terminar de leer" msgid "Want to read" msgstr "Quiero leer" -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:48 -#, fuzzy, python-format -#| msgid "Lists: %(username)s" +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:45 +#, python-format msgid "Remove from %(name)s" -msgstr "Listas: %(username)s" +msgstr "Quitar de %(name)s" #: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:5 #, python-format @@ -2492,98 +2626,79 @@ msgstr "Empezar \"%(book_title)s\"" msgid "Want to Read \"%(book_title)s\"" msgstr "Quiero leer \"%(book_title)s\"" +#: bookwyrm/templates/snippets/status/content_status.html:67 +#: bookwyrm/templates/snippets/trimmed_text.html:14 +msgid "Show more" +msgstr "Mostrar más" + +#: bookwyrm/templates/snippets/status/content_status.html:82 +#: bookwyrm/templates/snippets/trimmed_text.html:29 +msgid "Show less" +msgstr "Mostrar menos" + +#: bookwyrm/templates/snippets/status/content_status.html:112 +msgid "Open image in new window" +msgstr "Abrir imagen en una nueva ventana" + +#: bookwyrm/templates/snippets/status/layout.html:22 +#: bookwyrm/templates/snippets/status/status_options.html:17 +msgid "Delete status" +msgstr "Eliminar status" + +#: bookwyrm/templates/snippets/status/layout.html:52 +#: bookwyrm/templates/snippets/status/layout.html:53 +msgid "Boost status" +msgstr "Status de respaldo" + +#: bookwyrm/templates/snippets/status/layout.html:56 +#: bookwyrm/templates/snippets/status/layout.html:57 +msgid "Like status" +msgstr "Me gusta status" + #: bookwyrm/templates/snippets/status/status.html:9 msgid "boosted" msgstr "respaldó" -#: bookwyrm/templates/snippets/status/status_body.html:27 -#: bookwyrm/templates/snippets/status/status_options.html:18 -msgid "Delete status" -msgstr "Eliminar status" - -#: bookwyrm/templates/snippets/status/status_body.html:34 -#: bookwyrm/templates/snippets/status/status_body.html:47 -#: bookwyrm/templates/snippets/status/status_body.html:48 -msgid "Reply" -msgstr "Respuesta" - -#: bookwyrm/templates/snippets/status/status_content.html:18 -#: bookwyrm/templates/snippets/trimmed_text.html:15 -msgid "Show more" -msgstr "Mostrar más" - -#: bookwyrm/templates/snippets/status/status_content.html:25 -#: bookwyrm/templates/snippets/trimmed_text.html:25 -msgid "Show less" -msgstr "Mostrar menos" - -#: bookwyrm/templates/snippets/status/status_content.html:46 -msgid "Open image in new window" -msgstr "Abrir imagen en una nueva ventana" - -#: bookwyrm/templates/snippets/status/status_header.html:22 -#, fuzzy, python-format -#| msgid "Added by %(username)s" -msgid "replied to %(username)s's review" -msgstr "Agregado por %(username)s" - -#: bookwyrm/templates/snippets/status/status_header.html:24 -#, fuzzy, python-format -#| msgid "replied to your status" -msgid "replied to %(username)s's comment" -msgstr "respondió a tu status" - -#: bookwyrm/templates/snippets/status/status_header.html:26 -#, fuzzy, python-format -#| msgid "replied to your status" -msgid "replied to %(username)s's quote" -msgstr "respondió a tu status" - -#: bookwyrm/templates/snippets/status/status_header.html:28 -#, fuzzy, python-format -#| msgid "replied to your status" -msgid "replied to %(username)s's status" -msgstr "respondió a tu status" +#: bookwyrm/templates/snippets/status/status_header.html:44 +#, python-format +msgid "" +"replied to %(username)s's status" +msgstr "" +"respondió al status de %(username)s " #: bookwyrm/templates/snippets/status/status_options.html:7 #: bookwyrm/templates/snippets/user_options.html:7 msgid "More options" msgstr "Más opciones" +#: bookwyrm/templates/snippets/status/status_options.html:26 +msgid "Delete & re-draft" +msgstr "Eliminar y recomponer" + +#: bookwyrm/templates/snippets/status/status_options.html:35 +#: bookwyrm/templates/snippets/user_options.html:13 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:6 +msgid "Send direct message" +msgstr "Enviar mensaje directo" + #: bookwyrm/templates/snippets/switch_edition_button.html:5 msgid "Switch to this edition" msgstr "Cambiar a esta edición" #: bookwyrm/templates/snippets/table-sort-header.html:6 -#, fuzzy -#| msgid "Started reading" msgid "Sorted ascending" -msgstr "Lectura se empezó" +msgstr "En orden ascendente" #: bookwyrm/templates/snippets/table-sort-header.html:10 -#, fuzzy -#| msgid "Started reading" msgid "Sorted descending" -msgstr "Lectura se empezó" - -#: bookwyrm/templates/snippets/tag.html:14 -msgid "Remove tag" -msgstr "Eliminar etiqueta" - -#: bookwyrm/templates/snippets/tag.html:18 -msgid "Add tag" -msgstr "Agregar etiqueta" - -#: bookwyrm/templates/tag.html:9 -#, python-format -msgid "Books tagged \"%(tag.name)s\"" -msgstr "Libros etiquetados con \"%(tag.name)s\"" +msgstr "En orden descendente" #: bookwyrm/templates/user/books_header.html:5 -#, fuzzy, python-format -#| msgid "%(username)s's %(year)s Books" +#, python-format msgid "%(username)s's books" -msgstr "Los libros de %(username)s para %(year)s" +msgstr "Los libros de %(username)s" #: bookwyrm/templates/user/create_shelf_form.html:5 #: bookwyrm/templates/user/create_shelf_form.html:22 @@ -2618,10 +2733,6 @@ msgstr "Siguiendo" msgid "%(username)s isn't following any users" msgstr "%(username)s no sigue a nadie" -#: bookwyrm/templates/user/lists.html:9 -msgid "Your Lists" -msgstr "Tus listas" - #: bookwyrm/templates/user/lists.html:11 #, python-format msgid "Lists: %(username)s" @@ -2631,11 +2742,9 @@ msgstr "Listas: %(username)s" msgid "Create list" msgstr "Crear lista" -#: bookwyrm/templates/user/shelf.html:24 bookwyrm/views/shelf.py:56 -#, fuzzy -#| msgid "books" +#: bookwyrm/templates/user/shelf.html:24 bookwyrm/views/shelf.py:51 msgid "All books" -msgstr "libros" +msgstr "Todos los libros" #: bookwyrm/templates/user/shelf.html:37 msgid "Create shelf" @@ -2645,23 +2754,23 @@ msgstr "Crear estante" msgid "Edit shelf" msgstr "Editar estante" -#: bookwyrm/templates/user/shelf.html:78 +#: bookwyrm/templates/user/shelf.html:77 bookwyrm/templates/user/shelf.html:99 msgid "Shelved" msgstr "Archivado" -#: bookwyrm/templates/user/shelf.html:79 +#: bookwyrm/templates/user/shelf.html:78 bookwyrm/templates/user/shelf.html:103 msgid "Started" msgstr "Empezado" -#: bookwyrm/templates/user/shelf.html:80 +#: bookwyrm/templates/user/shelf.html:79 bookwyrm/templates/user/shelf.html:106 msgid "Finished" msgstr "Terminado" -#: bookwyrm/templates/user/shelf.html:127 +#: bookwyrm/templates/user/shelf.html:132 msgid "This shelf is empty." msgstr "Este estante está vacio." -#: bookwyrm/templates/user/shelf.html:133 +#: bookwyrm/templates/user/shelf.html:138 msgid "Delete shelf" msgstr "Eliminar estante" @@ -2670,14 +2779,13 @@ msgid "Edit profile" msgstr "Editar perfil" #: bookwyrm/templates/user/user.html:34 -#, fuzzy, python-format -#| msgid "See all %(size)s" +#, python-format msgid "View all %(size)s" -msgstr "Ver %(size)s" +msgstr "Ver todos los %(size)s" #: bookwyrm/templates/user/user.html:47 msgid "View all books" -msgstr "" +msgstr "Ver todos los libros" #: bookwyrm/templates/user/user.html:59 #, python-format @@ -2705,10 +2813,8 @@ msgid "Reading Goal" msgstr "Meta de lectura" #: bookwyrm/templates/user/user_layout.html:68 -#, fuzzy -#| msgid "Book" msgid "Books" -msgstr "Libro" +msgstr "Libros" #: bookwyrm/templates/user/user_preview.html:13 #, python-format @@ -2727,23 +2833,1535 @@ msgstr[1] "%(counter)s seguidores" msgid "%(counter)s following" msgstr "%(counter)s siguiendo" +#: bookwyrm/templates/user_admin/user.html:11 +msgid "Back to users" +msgstr "Volver a usuarios" + +#: bookwyrm/templates/user_admin/user_admin.html:7 +#, python-format +msgid "Users: %(server_name)s" +msgstr "Usuarios %(server_name)s" + +#: bookwyrm/templates/user_admin/user_admin.html:22 +#: bookwyrm/templates/user_admin/username_filter.html:5 +msgid "Username" +msgstr "Nombre de usuario" + +#: bookwyrm/templates/user_admin/user_admin.html:26 +msgid "Date Added" +msgstr "Fecha agregada" + +#: bookwyrm/templates/user_admin/user_admin.html:30 +msgid "Last Active" +msgstr "Actividad reciente" + +#: bookwyrm/templates/user_admin/user_admin.html:38 +msgid "Remote server" +msgstr "Quitar servidor" + +#: bookwyrm/templates/user_admin/user_admin.html:47 +msgid "Active" +msgstr "Activ@" + +#: bookwyrm/templates/user_admin/user_admin.html:47 +msgid "Inactive" +msgstr "Inactiv@" + +#: bookwyrm/templates/user_admin/user_admin.html:52 +#: bookwyrm/templates/user_admin/user_info.html:49 +msgid "Not set" +msgstr "No establecido" + +#: bookwyrm/templates/user_admin/user_info.html:5 +msgid "User details" +msgstr "Detalles" + +#: bookwyrm/templates/user_admin/user_info.html:14 +msgid "View user profile" +msgstr "Ver perfil de usuario" + +#: bookwyrm/templates/user_admin/user_info.html:20 +msgid "Instance details" +msgstr "Detalles de instancia" + +#: bookwyrm/templates/user_admin/user_info.html:46 +msgid "View instance" +msgstr "Ver instancia" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:11 +msgid "Suspend user" +msgstr "Suspender usuario" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:13 +msgid "Un-suspend user" +msgstr "Des-suspender usuario" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:21 +msgid "Access level:" +msgstr "Nivel de acceso:" + #: bookwyrm/views/password.py:32 -#, fuzzy -#| msgid "A user with that username already exists." msgid "No user with that email address was found." -msgstr "Ya existe un usuario con ese nombre." +msgstr "" +"No se pudo encontrar un usuario con esa dirección de correo electrónico." #: bookwyrm/views/password.py:41 #, python-format msgid "A password reset link sent to %s" +msgstr "Un enlace para reestablecer tu contraseña se enviará a %s" + +#: venv3/lib/python3.8/site-packages/_pytest/config/argparsing.py:442 +#, python-format +msgid "ambiguous option: %(option)s could match %(matches)s" +msgstr "opción ambiguo: %(option)s pudiera coincidir con %(matches)s" + +#: venv3/lib/python3.8/site-packages/django/contrib/messages/apps.py:7 +msgid "Messages" +msgstr "Mensajes" + +#: venv3/lib/python3.8/site-packages/django/contrib/sitemaps/apps.py:7 +msgid "Site Maps" +msgstr "Mapas de sitio" + +#: venv3/lib/python3.8/site-packages/django/contrib/staticfiles/apps.py:9 +msgid "Static Files" +msgstr "Archivos estáticos" + +#: venv3/lib/python3.8/site-packages/django/contrib/syndication/apps.py:7 +msgid "Syndication" +msgstr "Sindicación" + +#: venv3/lib/python3.8/site-packages/django/core/paginator.py:45 +msgid "That page number is not an integer" +msgstr "Ese numero de pagina no es un entero" + +#: venv3/lib/python3.8/site-packages/django/core/paginator.py:47 +msgid "That page number is less than 1" +msgstr "Ese numero de pagina es menos que uno" + +#: venv3/lib/python3.8/site-packages/django/core/paginator.py:52 +msgid "That page contains no results" +msgstr "Esa pagina no contiene resultados" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:32 +msgid "Enter a valid value." +msgstr "Ingrese un valor válido." + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:103 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:659 +msgid "Enter a valid URL." +msgstr "Ingrese una URL válida." + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:155 +msgid "Enter a valid integer." +msgstr "Ingrese un entero válido." + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:166 +msgid "Enter a valid email address." +msgstr "Ingrese una dirección de correo electrónico válida." + +#. Translators: "letters" means latin letters: a-z and A-Z. +#: venv3/lib/python3.8/site-packages/django/core/validators.py:240 +msgid "" +"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." msgstr "" +"Ingrese un “slug” válido que consiste de letras, numeros, guiones bajos, o " +"guiones" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:247 +msgid "" +"Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " +"hyphens." +msgstr "" +"Ingrese un “slug” válido que consiste de letras Unicode, numeros, guiones " +"bajos, o guiones" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:256 +#: venv3/lib/python3.8/site-packages/django/core/validators.py:276 +msgid "Enter a valid IPv4 address." +msgstr "Ingrese una dirección IPv4 válida." + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:261 +#: venv3/lib/python3.8/site-packages/django/core/validators.py:277 +msgid "Enter a valid IPv6 address." +msgstr "Ingrese una dirección IPv6 válida." + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:271 +#: venv3/lib/python3.8/site-packages/django/core/validators.py:275 +msgid "Enter a valid IPv4 or IPv6 address." +msgstr "Ingrese una dirección IPv4 o IPv6 válida." + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:305 +msgid "Enter only digits separated by commas." +msgstr "Ingrese solo digitos separados por comas." + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:311 +#, python-format +msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." +msgstr "Asegura que este valor es %(limit_value)s (es %(show_value)s)." + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:343 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "Asegura que este valor es menor que o iguala a %(limit_value)s." + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:352 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "Asegura que este valor es más que o que iguala a %(limit_value)s." + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:362 +#, python-format +msgid "" +"Ensure this value has at least %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at least %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +"Verifica que este valor tiene por lo menos %(limit_value)d carácter. (Tiene " +"%(show_value)d).)" +msgstr[1] "" +"Verifica que este valor tiene por lo menos %(limit_value)d caracteres. " +"(Tiene %(show_value)d).)" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:377 +#, python-format +msgid "" +"Ensure this value has at most %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at most %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +"Verifica que este valor tiene a lo sumo %(limit_value)d carácter. (Tiene " +"%(show_value)d).)" +msgstr[1] "" +"Verifica que este valor tiene a lo sumo %(limit_value)d caracteres. (Tiene " +"%(show_value)d).)" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:396 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:291 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:326 +msgid "Enter a number." +msgstr "Ingrese un número." + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:398 +#, python-format +msgid "Ensure that there are no more than %(max)s digit in total." +msgid_plural "Ensure that there are no more than %(max)s digits in total." +msgstr[0] "Verifica que no hay más que %(max)s digito en total." +msgstr[1] "Verifica que no hay más que %(max)s digitos en total." + +# is +#: venv3/lib/python3.8/site-packages/django/core/validators.py:403 +#, python-format +msgid "Ensure that there are no more than %(max)s decimal place." +msgid_plural "Ensure that there are no more than %(max)s decimal places." +msgstr[0] "Verifica que no hay más que %(max)s cifra decimal." +msgstr[1] "Verifica que no hay más que %(max)s cifras decimales." + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:408 +#, python-format +msgid "" +"Ensure that there are no more than %(max)s digit before the decimal point." +msgid_plural "" +"Ensure that there are no more than %(max)s digits before the decimal point." +msgstr[0] "" +"Verifica que no hay más que %(max)s digito antes de la coma decimal." +msgstr[1] "" +"Verifica que no hay más que %(max)s digitos antes de la coma decimal." + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:470 +#, python-format +msgid "" +"File extension “%(extension)s” is not allowed. Allowed extensions are: " +"%(allowed_extensions)s." +msgstr "" +"No se permite la extensión de archivo “%(extension)s”. Extensiones " +"permitidas son: %(allowed_extensions)s." + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:522 +msgid "Null characters are not allowed." +msgstr "No se permiten caracteres nulos" + +#: venv3/lib/python3.8/site-packages/django/db/models/base.py:1181 +#: venv3/lib/python3.8/site-packages/django/forms/models.py:759 +msgid "and" +msgstr "y" + +#: venv3/lib/python3.8/site-packages/django/db/models/base.py:1183 +#, python-format +msgid "%(model_name)s with this %(field_labels)s already exists." +msgstr "Ya existe %(model_name)s con este %(field_labels)s." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:104 +#, python-format +msgid "Value %(value)r is not a valid choice." +msgstr "El valor %(value)s no es una opción válida." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:105 +msgid "This field cannot be null." +msgstr "Este campo no puede ser nulo." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:106 +msgid "This field cannot be blank." +msgstr "Este campo no puede ser vacio." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:107 +#, fuzzy, python-format +#| msgid "%(model_name)s with this %(field_label)s already exists." +msgid "%(model_name)s with this %(field_label)s already exists." +msgstr "Ya existe %(model_name)s con este %(field_labels)s." + +#. Translators: The 'lookup_type' is one of 'date', 'year' or 'month'. +#. Eg: "Title must be unique for pub_date year" +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:111 +#, python-format +msgid "" +"%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." +msgstr "" +"%(field_label)s deben ser unicos por %(date_field_label)s %(lookup_type)s." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:130 +#, python-format +msgid "Field of type: %(field_type)s" +msgstr "Campo de tipo: %(field_type)s" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:937 +#, python-format +msgid "“%(value)s” value must be either True or False." +msgstr "“%(value)s” valor debe ser o verdadero o falso." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:938 +#, python-format +msgid "“%(value)s” value must be either True, False, or None." +msgstr "%(value)s” valor debe ser o True, False, o None." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:940 +msgid "Boolean (Either True or False)" +msgstr "Booleano (O True O False)" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:981 +#, python-format +msgid "String (up to %(max_length)s)" +msgstr "Cadena (máximo de %(max_length)s caracteres)" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1045 +msgid "Comma-separated integers" +msgstr "Enteros separados por comas" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1094 +#, python-format +msgid "" +"“%(value)s” value has an invalid date format. It must be in YYYY-MM-DD " +"format." +msgstr "" +"“%(value)s” valor tiene un formato de fecha inválido. Hay que estar de " +"formato YYYY-MM-DD." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1096 +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1239 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD) but it is an invalid " +"date." +msgstr "" +"“%(value)s” valor tiene el formato correcto (YYYY-MM-DD) pero la fecha es " +"invalida." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1099 +msgid "Date (without time)" +msgstr "Fecha (sin la hora)" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1237 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." +"uuuuuu]][TZ] format." +msgstr "" +"“%(value)s” valor tiene un formato invalido. Debe estar en formato YYYY-MM-" +"DD HH:MM[:ss[.uuuuuu]][TZ]." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1241 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" +"[TZ]) but it is an invalid date/time." +msgstr "" +"“%(value)s” valor tiene el formato correcto (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" +"[TZ]) pero es una fecha/hora invalida." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1245 +msgid "Date (with time)" +msgstr "Fecha (con la hora)" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1393 +#, python-format +msgid "“%(value)s” value must be a decimal number." +msgstr "El valor de “%(value)s” debe ser un número decimal." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1395 +msgid "Decimal number" +msgstr "Número decimal" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1534 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in [DD] [[HH:]MM:]ss[." +"uuuuuu] format." +msgstr "" +"“%(value)s” valor tiene un formato invalido. Debe estar en formato [DD] " +"[[HH:]MM:]ss[.uuuuuu]." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1537 +msgid "Duration" +msgstr "Duración" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1587 +msgid "Email address" +msgstr "Dirección de correo electrónico" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1610 +msgid "File path" +msgstr "Ruta de archivo" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1676 +#, python-format +msgid "“%(value)s” value must be a float." +msgstr "%(value)s no es un usuario válido" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1678 +msgid "Floating point number" +msgstr "Número de coma flotante" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1716 +#, python-format +msgid "“%(value)s” value must be an integer." +msgstr "“%(value)s” valor debe ser un entero." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1718 +msgid "Integer" +msgstr "Entero" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1801 +msgid "Big (8 byte) integer" +msgstr "Entero grande (8 byte)" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1817 +msgid "IPv4 address" +msgstr "Dirección IPv4" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1848 +msgid "IP address" +msgstr "Dirección IP" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1928 +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1929 +#, python-format +msgid "“%(value)s” value must be either None, True or False." +msgstr "Valor “%(value)s” debe ser o None, True, o False." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1931 +msgid "Boolean (Either True, False or None)" +msgstr "Booleano (O True, Falso, o None)" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1966 +msgid "Positive integer" +msgstr "Entero positivo" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1979 +msgid "Positive small integer" +msgstr "Entero positivo pequeño " + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1993 +#, python-format +msgid "Slug (up to %(max_length)s)" +msgstr "Slug (máximo de %(max_length)s)" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2025 +msgid "Small integer" +msgstr "Entero pequeño" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2032 +msgid "Text" +msgstr "Texto" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2060 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] " +"format." +msgstr "" +"“%(value)s” valor tiene un formato invalido. Debe estar en formato HH:MM[:" +"ss[.uuuuuu]]." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2062 +#, python-format +msgid "" +"“%(value)s” value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an " +"invalid time." +msgstr "" +"“%(value)s” valor tiene el formato correcto (HH:MM[:ss[.uuuuuu]]) pero es " +"una hora invalida." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2065 +msgid "Time" +msgstr "Tiempo" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2191 +msgid "URL" +msgstr "URL" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2213 +msgid "Raw binary data" +msgstr "Datos binarios sin procesar" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2278 +#, python-format +msgid "“%(value)s” is not a valid UUID." +msgstr "%(value)s no es una UUID válida." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2280 +msgid "Universally unique identifier" +msgstr "Identificador universalmente único" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/files.py:221 +msgid "File" +msgstr "Archivo" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/files.py:360 +msgid "Image" +msgstr "Imágen" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:778 +#, python-format +msgid "%(model)s instance with %(field)s %(value)r does not exist." +msgstr "%(model)s instancia con %(field)s %(value)r no existe." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:780 +msgid "Foreign Key (type determined by related field)" +msgstr "Clave externa (tipo determinado por campo relacionado)" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1012 +msgid "One-to-one relationship" +msgstr "Relación uno-a-uno" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1066 +#, python-format +msgid "%(from)s-%(to)s relationship" +msgstr "relación %(from)s-%(to)s" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1067 +#, python-format +msgid "%(from)s-%(to)s relationships" +msgstr "relaciones %(from)s-%(to)s" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1109 +msgid "Many-to-many relationship" +msgstr "Relaciones mucho-a-mucho" + +#. Translators: If found as last label character, these punctuation +#. characters will prevent the default label_suffix to be appended to the label +#: venv3/lib/python3.8/site-packages/django/forms/boundfield.py:149 +msgid ":?.!" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:53 +msgid "This field is required." +msgstr "Este campo es requerido." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:246 +msgid "Enter a whole number." +msgstr "Ingrese un número entero." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:397 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:1127 +msgid "Enter a valid date." +msgstr "Ingrese una fecha válida." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:421 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:1128 +msgid "Enter a valid time." +msgstr "Ingrese una hora válida." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:443 +msgid "Enter a valid date/time." +msgstr "Ingrese una fecha/hora válida." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:472 +msgid "Enter a valid duration." +msgstr "Ingrese una duración válida." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:473 +#, python-brace-format +msgid "The number of days must be between {min_days} and {max_days}." +msgstr "El número de dias debe ser entre {min_days} y {max_days}." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:533 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "" +"No se aceptó ningun archivo. Verfica el tipo de codificación en el " +"formulario." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:534 +msgid "No file was submitted." +msgstr "No se aceptó ningun archivo." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:535 +msgid "The submitted file is empty." +msgstr "El archivo enviado está vacio." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:537 +#, python-format +msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." +msgid_plural "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr[0] "" +"Verifica que este nombre de archivo no tiene más que %(max)d carácter. " +"(Tiene %(length)d)." +msgstr[1] "" +"Verifica que este nombre de archivo no tiene más que %(max)d caracteres. " +"(Tiene %(length)d)." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:540 +msgid "Please either submit a file or check the clear checkbox, not both." +msgstr "Por favor, o envia un archivo o marca la casilla vacia, no los dos." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:601 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" +"Subir una imagen válida. El archivo que subiste o no fue imagen o fue " +"corrupto." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:763 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:853 +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1275 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "" +"Selecciona una opción válida. %(value)s no es una de las opciones " +"disponibles." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:854 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:969 +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1274 +msgid "Enter a list of values." +msgstr "Ingrese una lista de valores." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:970 +msgid "Enter a complete value." +msgstr "Ingresa un valor completo." + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:1186 +msgid "Enter a valid UUID." +msgstr "Ingrese una UUID válida." + +#. Translators: This is the default suffix added to form field labels +#: venv3/lib/python3.8/site-packages/django/forms/forms.py:81 +msgid ":" +msgstr ":" + +#: venv3/lib/python3.8/site-packages/django/forms/forms.py:207 +#, python-format +msgid "(Hidden field %(name)s) %(error)s" +msgstr "(Campo oculto %(name)s) %(error)s" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:93 +msgid "ManagementForm data is missing or has been tampered with" +msgstr "Datos de ManagementForm está ausento o ha sido corrompido" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:345 +#, python-format +msgid "Please submit %d or fewer forms." +msgid_plural "Please submit %d or fewer forms." +msgstr[0] "Por favor, enviar %d o menos formularios." +msgstr[1] "Por favor, enviar %d o menos formularios." + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:352 +#, python-format +msgid "Please submit %d or more forms." +msgid_plural "Please submit %d or more forms." +msgstr[0] "Por favor, enviar %d o más formularios." +msgstr[1] "Por favor, enviar %d o más formularios." + +# TODO cc @mouse is this a verb or noun +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:379 +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:386 +#, fuzzy +msgid "Order" +msgstr "Pedir" + +# if verb +# msgstr "Pedido" # if noun +#: venv3/lib/python3.8/site-packages/django/forms/models.py:754 +#, python-format +msgid "Please correct the duplicate data for %(field)s." +msgstr "Por favor corrige los datos duplicados en %(field)s." + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:758 +#, python-format +msgid "Please correct the duplicate data for %(field)s, which must be unique." +msgstr "" +"Por favor corrige los datos duplicados en %(field)s, los cuales deben ser " +"unicos." + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:764 +#, python-format +msgid "" +"Please correct the duplicate data for %(field_name)s which must be unique " +"for the %(lookup)s in %(date_field)s." +msgstr "" +"Por favor corrige los datos duplicados en %(field_name)s los cuales deben " +"ser unicos por el %(lookup)s en %(date_field)s." + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:773 +msgid "Please correct the duplicate values below." +msgstr "Por favor corrige los valores duplicados a continuación." + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1094 +msgid "The inline value did not match the parent instance." +msgstr "El valor en línea no empareja la instancia progenitor." + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1161 +msgid "Select a valid choice. That choice is not one of the available choices." +msgstr "" +"Selecciona una opción válida. Esa opción no es una de las opciones " +"disponibles." + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1277 +#, python-format +msgid "“%(pk)s” is not a valid value." +msgstr "“%(pk)s” no es un valor válido." + +#: venv3/lib/python3.8/site-packages/django/forms/utils.py:162 +#, python-format +msgid "" +"%(datetime)s couldn’t be interpreted in time zone %(current_timezone)s; it " +"may be ambiguous or it may not exist." +msgstr "" +"%(datetime)s no se pudo interpretar en la zona horaria %(current_timezone)s; " +"puede ser ambiguo o puede que no exista." + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:395 +msgid "Clear" +msgstr "Borrar" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:396 +msgid "Currently" +msgstr "Actualmente" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:397 +msgid "Change" +msgstr "Cambiar" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:709 +msgid "Unknown" +msgstr "Desconocido" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:710 +msgid "Yes" +msgstr "Sí" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:711 +msgid "No" +msgstr "No" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:788 +msgid "yes,no,maybe" +msgstr "sí,no,quizás" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:817 +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:834 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "%(size)d byte" +msgstr[1] "%(size)d bytes" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:836 +#, python-format +msgid "%s KB" +msgstr "%s KB" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:838 +#, python-format +msgid "%s MB" +msgstr "%s MB" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:840 +#, python-format +msgid "%s GB" +msgstr "%s GB" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:842 +#, python-format +msgid "%s TB" +msgstr "%s TB" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:844 +#, python-format +msgid "%s PB" +msgstr "%s PB" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:62 +msgid "p.m." +msgstr "p.m." + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:63 +msgid "a.m." +msgstr "a.m." + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:68 +msgid "PM" +msgstr "PM" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:69 +msgid "AM" +msgstr "AM" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:150 +msgid "midnight" +msgstr "medianoche" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:152 +msgid "noon" +msgstr "mediodia" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:295 +msgid "Monday" +msgstr "Lunes" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:296 +msgid "Tuesday" +msgstr "Martes" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:297 +msgid "Wednesday" +msgstr "Miercoles" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:298 +msgid "Thursday" +msgstr "Jueves" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:299 +msgid "Friday" +msgstr "Viernes" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:7 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:300 +msgid "Saturday" +msgstr "Sábado" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:7 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:301 +msgid "Sunday" +msgstr "Domino" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Mon" +msgstr "Lun" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Tue" +msgstr "Mar" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Wed" +msgstr "Mie" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Thu" +msgstr "Jue" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Fri" +msgstr "Vie" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:11 +msgid "Sat" +msgstr "Sáb" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:11 +msgid "Sun" +msgstr "Dom" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:281 +msgid "January" +msgstr "Enero" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:282 +msgid "February" +msgstr "Febrero" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:283 +msgid "March" +msgstr "Marzo" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:284 +msgid "April" +msgstr "Abril" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:285 +msgid "May" +msgstr "Mayo" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:286 +msgid "June" +msgstr "Junio" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:287 +msgid "July" +msgstr "Julio" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:288 +msgid "August" +msgstr "Agosto" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:289 +msgid "September" +msgstr "Septiembre" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:290 +msgid "October" +msgstr "Octubre" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:291 +msgid "November" +msgstr "Noviembre" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:16 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:292 +msgid "December" +msgstr "Diciembre" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "jan" +msgstr "ene" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "feb" +msgstr "feb" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "mar" +msgstr "mar" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "apr" +msgstr "abr" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "may" +msgstr "may" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "jun" +msgstr "jun" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "jul" +msgstr "jul" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "aug" +msgstr "ago" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "sep" +msgstr "sep" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "oct" +msgstr "oct" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "nov" +msgstr "nov" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "dec" +msgstr "dic" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:23 +msgctxt "abbrev. month" +msgid "Jan." +msgstr "en." + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:24 +msgctxt "abbrev. month" +msgid "Feb." +msgstr "feb." + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:25 +msgctxt "abbrev. month" +msgid "March" +msgstr "mzo." + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:26 +msgctxt "abbrev. month" +msgid "April" +msgstr "abr." + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:27 +msgctxt "abbrev. month" +msgid "May" +msgstr "my." + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:28 +msgctxt "abbrev. month" +msgid "June" +msgstr "jun." + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:29 +msgctxt "abbrev. month" +msgid "July" +msgstr "jul." + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:30 +msgctxt "abbrev. month" +msgid "Aug." +msgstr "agto." + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:31 +msgctxt "abbrev. month" +msgid "Sept." +msgstr "set." + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:32 +msgctxt "abbrev. month" +msgid "Oct." +msgstr "oct." + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:33 +msgctxt "abbrev. month" +msgid "Nov." +msgstr "nov." + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:34 +msgctxt "abbrev. month" +msgid "Dec." +msgstr "dic." + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:37 +msgctxt "alt. month" +msgid "January" +msgstr "Enero" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:38 +msgctxt "alt. month" +msgid "February" +msgstr "Febrero" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:39 +msgctxt "alt. month" +msgid "March" +msgstr "Marzo" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:40 +msgctxt "alt. month" +msgid "April" +msgstr "Abril" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:41 +msgctxt "alt. month" +msgid "May" +msgstr "Mayo" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:42 +msgctxt "alt. month" +msgid "June" +msgstr "Junio" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:43 +msgctxt "alt. month" +msgid "July" +msgstr "Julio" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:44 +msgctxt "alt. month" +msgid "August" +msgstr "Agosto" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:45 +msgctxt "alt. month" +msgid "September" +msgstr "Septiembre" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:46 +msgctxt "alt. month" +msgid "October" +msgstr "Octubre" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:47 +msgctxt "alt. month" +msgid "November" +msgstr "Noviembre" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:48 +msgctxt "alt. month" +msgid "December" +msgstr "Diciembre" + +#: venv3/lib/python3.8/site-packages/django/utils/ipv6.py:8 +msgid "This is not a valid IPv6 address." +msgstr "Esta no es una dirección IPv6 válida." + +#: venv3/lib/python3.8/site-packages/django/utils/text.py:69 +#, python-format +msgctxt "String to return when truncating text" +msgid "%(truncated_text)s…" +msgstr "%(truncated_text)s…" + +#: venv3/lib/python3.8/site-packages/django/utils/text.py:235 +msgid "or" +msgstr "o" + +#. Translators: This string is used as a separator between list elements +#: venv3/lib/python3.8/site-packages/django/utils/text.py:254 +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:83 +msgid ", " +msgstr ", " + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:9 +#, python-format +msgid "%d year" +msgid_plural "%d years" +msgstr[0] "%d año" +msgstr[1] "%d años" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:10 +#, python-format +msgid "%d month" +msgid_plural "%d months" +msgstr[0] "%d mes" +msgstr[1] "%d meses" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:11 +#, python-format +msgid "%d week" +msgid_plural "%d weeks" +msgstr[0] "%d semana" +msgstr[1] "%d semanas" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:12 +#, python-format +msgid "%d day" +msgid_plural "%d days" +msgstr[0] "%d día" +msgstr[1] "%d días" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:13 +#, python-format +msgid "%d hour" +msgid_plural "%d hours" +msgstr[0] "%d hora" +msgstr[1] "%d horas" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:14 +#, python-format +msgid "%d minute" +msgid_plural "%d minutes" +msgstr[0] "%d minuto" +msgstr[1] "%d minutos" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:72 +#, fuzzy +#| msgid "%d minute" +#| msgid_plural "%d minutes" +msgid "0 minutes" +msgstr "%d minuto" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:110 +msgid "Forbidden" +msgstr "Prohibido" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:111 +msgid "CSRF verification failed. Request aborted." +msgstr "Se falló la verificación CSRF. Se abortó la solicitud." + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:115 +msgid "" +"You are seeing this message because this HTTPS site requires a “Referer " +"header” to be sent by your Web browser, but none was sent. This header is " +"required for security reasons, to ensure that your browser is not being " +"hijacked by third parties." +msgstr "" +"Estás viendo este mensaje porque este sitio HTTPS requiere que tu navegador " +"Web envie un “Referer header”, pero no se la envió. Esta cabecedera se " +"requiere por razones de seguridad, para asegurar que tu navegador no sea " +"secuestrado por terceros." + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:120 +msgid "" +"If you have configured your browser to disable “Referer” headers, please re-" +"enable them, at least for this site, or for HTTPS connections, or for “same-" +"origin” requests." +msgstr "" +"Si has configurado su navegador para deshabilitar las cabecederas “Referer”, " +"vuelva a habilitarlos, al menos para este sitio, o para conexiones HTTPS, o " +"para solicitudes del “same-origin”. " + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:124 +msgid "" +"If you are using the tag or " +"including the “Referrer-Policy: no-referrer” header, please remove them. The " +"CSRF protection requires the “Referer” header to do strict referer checking. " +"If you’re concerned about privacy, use alternatives like for links to third-party sites." +msgstr "" +"Si estás usando la eqtigueta o estás incluyendo la cabecedera “Referrer-Policy: no-referrer”, " +"quitalas por favor. La protección CSRF require la cabecedera “Referer” para " +"hacer verficación “strict referer“. Si te preocupa la privacidad, utiliza " +"alternativas como para sitios de terceros." + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:132 +msgid "" +"You are seeing this message because this site requires a CSRF cookie when " +"submitting forms. This cookie is required for security reasons, to ensure " +"that your browser is not being hijacked by third parties." +msgstr "" +"Estás viendo este mensaje porque este sitio requiere un cookie CSRF cuando " +"se envie formularios. Este cookie se requiere por razones de seguridad, para " +"asegurar que tu navegador no sea secuestrado por terceros." + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:137 +msgid "" +"If you have configured your browser to disable cookies, please re-enable " +"them, at least for this site, or for “same-origin” requests." +msgstr "" +"Si has configurado su navegador para deshabilitar los cookies, vuelva a " +"habilitarlos, al menos para este sitio, o para conexiones HTTPS, o para " +"solicitudes del “same-origin”. " + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:142 +msgid "More information is available with DEBUG=True." +msgstr "Más información es disponible con DEBUG=True." + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:41 +msgid "No year specified" +msgstr "Ningun año fue especificado" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:61 +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:111 +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:208 +msgid "Date out of range" +msgstr "Fecha fuera de rango" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:90 +msgid "No month specified" +msgstr "Ningun mes fue especificado" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:142 +msgid "No day specified" +msgstr "Ningun día fue especificado" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:188 +msgid "No week specified" +msgstr "Ninguna semana fue especificado" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:338 +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:367 +#, python-format +msgid "No %(verbose_name_plural)s available" +msgstr "No %(verbose_name_plural)s disponible" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:589 +#, python-format +msgid "" +"Future %(verbose_name_plural)s not available because %(class_name)s." +"allow_future is False." +msgstr "" +"%(verbose_name_plural)s del futuro no está disponible porque %(class_name)s." +"allow_future es False." + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:623 +#, python-format +msgid "Invalid date string “%(datestr)s” given format “%(format)s”" +msgstr "Cadena de fecha invalida “%(datestr)s” dado el formato “%(format)s”" + +#: venv3/lib/python3.8/site-packages/django/views/generic/detail.py:54 +#, python-format +msgid "No %(verbose_name)s found matching the query" +msgstr "No se encontró ningún %(verbose_name)s correspondiente a la búsqueda" + +#: venv3/lib/python3.8/site-packages/django/views/generic/list.py:67 +msgid "Page is not “last”, nor can it be converted to an int." +msgstr "Página no es “last”, ni puede ser convertido en un int." + +#: venv3/lib/python3.8/site-packages/django/views/generic/list.py:72 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "Página invalida (%(page_number)s): %(message)s" + +#: venv3/lib/python3.8/site-packages/django/views/generic/list.py:154 +#, python-format +msgid "Empty list and “%(class_name)s.allow_empty” is False." +msgstr "Lista vacia y “%(class_name)s.allow_empty” es False." + +#: venv3/lib/python3.8/site-packages/django/views/static.py:40 +msgid "Directory indexes are not allowed here." +msgstr "Indices directorios no se permiten aquí." + +#: venv3/lib/python3.8/site-packages/django/views/static.py:42 +#, python-format +msgid "“%(path)s” does not exist" +msgstr "“%(path)s” no existe" + +#: venv3/lib/python3.8/site-packages/django/views/static.py:80 +#, python-format +msgid "Index of %(directory)s" +msgstr "Indice de %(directory)s" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:7 +msgid "Django: the Web framework for perfectionists with deadlines." +msgstr "Django: el estructura Web para perfeccionistas con fechas límites." + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:346 +#, python-format +msgid "" +"View release notes for Django %(version)s" +msgstr "" +"Ver notas de lanzamiento por Django " +"%(version)s" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:368 +msgid "The install worked successfully! Congratulations!" +msgstr "¡La instalación fue exitoso! ¡Felicidades!" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:369 +#, python-format +msgid "" +"You are seeing this page because DEBUG=True is in your settings file and you have not configured any " +"URLs." +msgstr "" +"Estás viendo esta pagina porque DEBUG=True está en tu archivo de configuración y no has configurado " +"ningún URL." + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:384 +msgid "Django Documentation" +msgstr "Documentación de Django" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:385 +msgid "Topics, references, & how-to’s" +msgstr "Tópicos, referencias, & instrucciones paso-a-paso" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:396 +msgid "Tutorial: A Polling App" +msgstr "Tutorial: Una aplicación polling" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:397 +msgid "Get started with Django" +msgstr "Empezar con Django" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:408 +msgid "Django Community" +msgstr "Comunidad Django" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:409 +msgid "Connect, get help, or contribute" +msgstr "Conectarse, encontrar ayuda, o contribuir" + +#: venv3/lib/python3.8/site-packages/kombu/transport/qpid.py:1301 +#, python-format +msgid "Attempting to connect to qpid with SASL mechanism %s" +msgstr "Intentando conectar con qpid con mecanismo SASL %s" + +#: venv3/lib/python3.8/site-packages/kombu/transport/qpid.py:1306 +#, python-format +msgid "Connected to qpid with SASL mechanism %s" +msgstr "Conectado con qpid con mecanismo SASL %s" + +#: venv3/lib/python3.8/site-packages/kombu/transport/qpid.py:1324 +#, python-format +msgid "Unable to connect to qpid with SASL mechanism %s" +msgstr "No se pudo conectar con qpid con mecanismo SASL %s" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:371 +msgid "1 second ago" +msgstr "Hace 1 segundo" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:377 +msgid "1 minute ago" +msgstr "Hace 1 minuto" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:382 +msgid "1 hour ago" +msgstr "Hace 1 hora" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:385 +#, python-format +msgid "%(time)s" +msgstr "%(time)s" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:387 +msgid "yesterday" +msgstr "ayer" + +# TODO cc @mouse this could be grammatically incorrect if the time said 1 o'clock +# a working clock is broken twice a day! +#: venv3/lib/python3.8/site-packages/tornado/locale.py:387 +#, python-format +msgid "yesterday at %(time)s" +msgstr "ayer a las %(time)s" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:389 +#, python-format +msgid "%(weekday)s" +msgstr "%(weekday)s" + +# TODO cc @mouse this could be grammatically incorrect if the time said 1 o'clock +# a working clock is broken twice a day! +#: venv3/lib/python3.8/site-packages/tornado/locale.py:389 +#, python-format +msgid "%(weekday)s at %(time)s" +msgstr "%(weekday)s a las %(time)s" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:392 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:445 +#, python-format +msgid "%(month_name)s %(day)s" +msgstr "%(day)s %(month_name)s" + +# TODO cc @mouse this could be grammatically incorrect if the time said 1 o'clock +# a working clock is broken twice a day! +#: venv3/lib/python3.8/site-packages/tornado/locale.py:394 +#, python-format +msgid "%(month_name)s %(day)s at %(time)s" +msgstr "%(day)s %(month_name)s a las %(time)s" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:399 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s" +msgstr "%(day)s %(month_name)s, %(year)s" + +# TODO cc @mouse this could be grammatically incorrect if the time said 1 o'clock +# a working clock is broken twice a day! +#: venv3/lib/python3.8/site-packages/tornado/locale.py:401 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s at %(time)s" +msgstr "%(day)s %(month_name)s, %(year)s a las %(time)s" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:439 +#, python-format +msgid "%(weekday)s, %(month_name)s %(day)s" +msgstr "%(weekday)s, %(day)s %(month_name)s" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:462 +#, python-format +msgid "%(commas)s and %(last)s" +msgstr "%(commas)s y %(last)s" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:68 +msgctxt "law" +msgid "right" +msgstr "justo" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:69 +msgctxt "good" +msgid "right" +msgstr "correcto" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:71 +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:74 +msgctxt "organization" +msgid "club" +msgstr "club" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:76 +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:77 +msgctxt "stick" +msgid "club" +msgstr "garrote" + +#, python-format +#~ msgid "%(rating)s star" +#~ msgid_plural "%(rating)s stars" +#~ msgstr[0] "%(rating)s estrella" +#~ msgstr[1] "%(rating)s estrellas" + +#, python-format +#~ msgid "" +#~ "replied to %(username)s's review" +#~ msgstr "" +#~ "respondió a la reseña de %(username)s " + +#, python-format +#~ msgid "" +#~ "replied to %(username)s's comment" +#~ msgstr "" +#~ "respondió al comentario de %(username)s " + +#, python-format +#~ msgid "" +#~ "replied to %(username)s's quote" +#~ msgstr "" +#~ "respondió a la cita de %(username)s " + +#~ msgid "Remove tag" +#~ msgstr "Eliminar etiqueta" + +#~ msgid "Add tag" +#~ msgstr "Agregar etiqueta" + +#~ msgid "Books tagged \"%(tag.name)s\"" +#~ msgstr "Libros etiquetados con \"%(tag.name)s\"" + +#~ msgid "Deactivate user" +#~ msgstr "Desactivar usuario" + +#~ msgid "Reactivate user" +#~ msgstr "Reactivar usuario" + +#~ msgid "Positive big integer" +#~ msgstr "Entero positivo grande" + +#~ msgid "A JSON object" +#~ msgstr "Un objeto JSON" + +#~ msgid "Value must be valid JSON." +#~ msgstr "Valor debe ser JSON válido." + +#~ msgid "Enter a valid JSON." +#~ msgstr "Ingrese una JSON válida." #, fuzzy #~| msgid "Started" #~ msgid "Getting Started" #~ msgstr "Empezado" -#, fuzzy, python-format +#, fuzzy #~| msgid "No users found for \"%(query)s\"" #~ msgid "No users were found for \"%(query)s\"" #~ msgstr "No se encontró ningún usuario correspondiente a \"%(query)s\"" @@ -2751,13 +4369,9 @@ msgstr "" #~ msgid "Tags" #~ msgstr "Etiquetas" -#~ msgid "Your shelves" -#~ msgstr "Tus estantes" - #~ msgid "Your lists" #~ msgstr "Tus listas" -#, python-format #~ msgid "See all %(size)s lists" #~ msgstr "Ver las %(size)s listas" @@ -2782,148 +4396,18 @@ msgstr "" #~ msgid "Your Shelves" #~ msgstr "Tus estantes" -#, python-format #~ msgid "%(username)s: Shelves" #~ msgstr "%(username)s: Estantes" #~ msgid "Shelves" #~ msgstr "Estantes" -#, python-format #~ msgid "See all %(shelf_count)s shelves" #~ msgstr "Ver los %(shelf_count)s estantes" #~ msgid "Send follow request" #~ msgstr "Envia solicitud de seguidor" -#, fuzzy -#~| msgid "All messages" -#~ msgid "Messages" -#~ msgstr "Todos los mensajes" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "Enter a valid email address." -#~ msgstr "Dirección de correo electrónico:" - -#, fuzzy -#~| msgid "Series number:" -#~ msgid "Enter a number." -#~ msgstr "Número de serie:" - -#, fuzzy -#~| msgid "%(value)s is not a valid remote_id" -#~ msgid "Value %(value)r is not a valid choice." -#~ msgstr "%(value)s no es un remote_id válido" - -#, fuzzy -#~| msgid "Series number:" -#~ msgid "Decimal number" -#~ msgstr "Número de serie:" - -#, fuzzy -#~| msgid "List curation:" -#~ msgid "Duration" -#~ msgstr "Enumerar lista de comisariado:" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "Email address" -#~ msgstr "Dirección de correo electrónico:" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "IPv4 address" -#~ msgstr "Dirección de correo electrónico:" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "IP address" -#~ msgstr "Dirección de correo electrónico:" - -#, fuzzy -#~| msgid "No active invites" -#~ msgid "Positive integer" -#~ msgstr "No invitaciónes activas" - -#, fuzzy -#~| msgid "%(value)s is not a valid username" -#~ msgid "“%(value)s” is not a valid UUID." -#~ msgstr "%(value)s no es un usuario válido" - -#, fuzzy -#~| msgid "Images" -#~ msgid "Image" -#~ msgstr "Imagenes" - -#, fuzzy -#~| msgid "Relationships" -#~ msgid "One-to-one relationship" -#~ msgstr "Relaciones" - -#, fuzzy -#~| msgid "This shelf is empty." -#~ msgid "This field is required." -#~ msgstr "Este estante está vacio." - -#, fuzzy -#~| msgid "This shelf is empty." -#~ msgid "The submitted file is empty." -#~ msgstr "Este estante está vacio." - -#, fuzzy -#~| msgid "%(value)s is not a valid username" -#~ msgid "“%(pk)s” is not a valid value." -#~ msgstr "%(value)s no es un usuario válido" - -#, fuzzy -#~| msgid "Currently Reading" -#~ msgid "Currently" -#~ msgstr "Leyendo actualmente" - -#, fuzzy -#~| msgid "Change shelf" -#~ msgid "Change" -#~ msgstr "Cambiar estante" - -#, fuzzy -#~| msgid "Status" -#~ msgid "Sat" -#~ msgstr "Status" - -#, fuzzy -#~| msgid "Search" -#~ msgid "March" -#~ msgstr "Buscar" - -#, fuzzy -#~| msgid "Series number:" -#~ msgid "September" -#~ msgstr "Número de serie:" - -#, fuzzy -#~| msgid "Search" -#~ msgctxt "abbrev. month" -#~ msgid "March" -#~ msgstr "Buscar" - -#, fuzzy -#~| msgid "Search" -#~ msgctxt "alt. month" -#~ msgid "March" -#~ msgstr "Buscar" - -#, fuzzy -#~| msgid "Series number:" -#~ msgctxt "alt. month" -#~ msgid "September" -#~ msgstr "Número de serie:" - -#, fuzzy -#~| msgid "No books found matching the query \"%(query)s\"" -#~ msgid "No %(verbose_name)s found matching the query" -#~ msgstr "No se encontró ningún libro correspondiente a la búsqueda: \"%(query)s\"" - #~ msgid "Announcements" #~ msgstr "Anuncios" diff --git a/locale/fr_FR/LC_MESSAGES/django.mo b/locale/fr_FR/LC_MESSAGES/django.mo index 09e90f22..b91a542d 100644 Binary files a/locale/fr_FR/LC_MESSAGES/django.mo and b/locale/fr_FR/LC_MESSAGES/django.mo differ diff --git a/locale/fr_FR/LC_MESSAGES/django.po b/locale/fr_FR/LC_MESSAGES/django.po index c765cb89..ee2f7936 100644 --- a/locale/fr_FR/LC_MESSAGES/django.po +++ b/locale/fr_FR/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-04-05 07:29+0000\n" +"POT-Creation-Date: 2021-04-26 09:56-0700\n" "PO-Revision-Date: 2021-04-05 12:44+0100\n" "Last-Translator: Fabien Basmaison \n" "Language-Team: Mouse Reeve \n" @@ -18,36 +18,66 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: bookwyrm/forms.py:226 +#: bookwyrm/forms.py:224 msgid "A user with this email already exists." msgstr "Cet email est déjà associé à un compte." -#: bookwyrm/forms.py:240 +#: bookwyrm/forms.py:238 msgid "One Day" msgstr "Un jour" -#: bookwyrm/forms.py:241 +#: bookwyrm/forms.py:239 msgid "One Week" msgstr "Une semaine" -#: bookwyrm/forms.py:242 +#: bookwyrm/forms.py:240 msgid "One Month" msgstr "Un mois" -#: bookwyrm/forms.py:243 +#: bookwyrm/forms.py:241 msgid "Does Not Expire" msgstr "Sans expiration" -#: bookwyrm/forms.py:248 +#: bookwyrm/forms.py:246 #, python-format msgid "%(count)d uses" msgstr "%(count)d utilisations" -#: bookwyrm/forms.py:251 -#| msgid "Unlisted" +#: bookwyrm/forms.py:249 msgid "Unlimited" msgstr "Sans limite" +#: bookwyrm/forms.py:293 +msgid "List Order" +msgstr "" + +#: bookwyrm/forms.py:294 +#, fuzzy +#| msgid "Title" +msgid "Book Title" +msgstr "Titre" + +#: bookwyrm/forms.py:295 bookwyrm/templates/snippets/create_status_form.html:31 +#: bookwyrm/templates/user/shelf.html:80 bookwyrm/templates/user/shelf.html:110 +msgid "Rating" +msgstr "Note" + +#: bookwyrm/forms.py:297 bookwyrm/templates/lists/list.html:82 +msgid "Sort By" +msgstr "" + +#: bookwyrm/forms.py:301 +#, fuzzy +#| msgid "Sorted ascending" +msgid "Ascending" +msgstr "Trié par ordre croissant" + +#: bookwyrm/forms.py:302 +#, fuzzy +#| msgid "Sorted ascending" +msgid "Descending" +msgstr "Trié par ordre croissant" + #: bookwyrm/models/fields.py:24 #, python-format msgid "%(value)s is not a valid remote_id" @@ -58,8 +88,7 @@ msgstr "%(value)s n’est pas une remote_id valide." msgid "%(value)s is not a valid username" msgstr "%(value)s n’est pas un nom de compte valide." -#: bookwyrm/models/fields.py:165 bookwyrm/templates/layout.html:152 -#| msgid "Username:" +#: bookwyrm/models/fields.py:165 bookwyrm/templates/layout.html:153 msgid "username" msgstr "nom du compte :" @@ -67,23 +96,23 @@ msgstr "nom du compte :" msgid "A user with that username already exists." msgstr "Ce nom est déjà associé à un compte." -#: bookwyrm/settings.py:150 +#: bookwyrm/settings.py:152 msgid "English" msgstr "English" -#: bookwyrm/settings.py:151 +#: bookwyrm/settings.py:153 msgid "German" msgstr "Deutsch" -#: bookwyrm/settings.py:152 +#: bookwyrm/settings.py:154 msgid "Spanish" msgstr "Español" -#: bookwyrm/settings.py:153 +#: bookwyrm/settings.py:155 msgid "French" msgstr "Français" -#: bookwyrm/settings.py:154 +#: bookwyrm/settings.py:156 msgid "Simplified Chinese" msgstr "简化字" @@ -120,79 +149,67 @@ msgstr "Wikipedia" msgid "Books by %(name)s" msgstr "Livres par %(name)s" -#: bookwyrm/templates/book/book.html:21 +#: bookwyrm/templates/book/book.html:33 #: bookwyrm/templates/discover/large-book.html:12 #: bookwyrm/templates/discover/small-book.html:9 msgid "by" msgstr "par" -#: bookwyrm/templates/book/book.html:29 bookwyrm/templates/book/book.html:30 +#: bookwyrm/templates/book/book.html:41 bookwyrm/templates/book/book.html:42 msgid "Edit Book" msgstr "Modifier le livre" -#: bookwyrm/templates/book/book.html:49 +#: bookwyrm/templates/book/book.html:61 #: bookwyrm/templates/book/cover_modal.html:5 msgid "Add cover" msgstr "Ajouter une couverture" -#: bookwyrm/templates/book/book.html:53 -#| msgid "Failed to load" +#: bookwyrm/templates/book/book.html:65 msgid "Failed to load cover" msgstr "La couverture n’a pu être chargée" -#: bookwyrm/templates/book/book.html:62 -msgid "ISBN:" -msgstr "ISBN :" - -#: bookwyrm/templates/book/book.html:69 -#: bookwyrm/templates/book/edit_book.html:217 -msgid "OCLC Number:" -msgstr "Numéro OCLC :" - -#: bookwyrm/templates/book/book.html:76 -#: bookwyrm/templates/book/edit_book.html:221 -msgid "ASIN:" -msgstr "ASIN :" - -#: bookwyrm/templates/book/book.html:85 +#: bookwyrm/templates/book/book.html:82 msgid "View on OpenLibrary" msgstr "Voir sur OpenLibrary" -#: bookwyrm/templates/book/book.html:94 +#: bookwyrm/templates/book/book.html:102 #, python-format msgid "(%(review_count)s review)" msgid_plural "(%(review_count)s reviews)" msgstr[0] "(%(review_count)s critique)" msgstr[1] "(%(review_count)s critiques)" -#: bookwyrm/templates/book/book.html:100 -#| msgid "Description:" +#: bookwyrm/templates/book/book.html:114 msgid "Add Description" msgstr "Ajouter une description" -#: bookwyrm/templates/book/book.html:107 -#: bookwyrm/templates/book/edit_book.html:101 +#: bookwyrm/templates/book/book.html:121 +#: bookwyrm/templates/book/edit_book.html:107 #: bookwyrm/templates/lists/form.html:12 msgid "Description:" msgstr "Description :" -#: bookwyrm/templates/book/book.html:111 -#: bookwyrm/templates/book/edit_book.html:231 +#: bookwyrm/templates/book/book.html:125 +#: bookwyrm/templates/book/edit_book.html:240 #: bookwyrm/templates/edit_author.html:78 bookwyrm/templates/lists/form.html:42 #: bookwyrm/templates/preferences/edit_user.html:70 +#: bookwyrm/templates/settings/edit_server.html:68 +#: bookwyrm/templates/settings/federated_server.html:93 #: bookwyrm/templates/settings/site.html:93 #: bookwyrm/templates/snippets/readthrough.html:75 #: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:42 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:42 #: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:34 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:38 msgid "Save" msgstr "Enregistrer" -#: bookwyrm/templates/book/book.html:112 bookwyrm/templates/book/book.html:161 +#: bookwyrm/templates/book/book.html:126 bookwyrm/templates/book/book.html:175 #: bookwyrm/templates/book/cover_modal.html:32 -#: bookwyrm/templates/book/edit_book.html:232 +#: bookwyrm/templates/book/edit_book.html:241 #: bookwyrm/templates/edit_author.html:79 -#: bookwyrm/templates/moderation/report_modal.html:32 +#: bookwyrm/templates/moderation/report_modal.html:34 +#: bookwyrm/templates/settings/federated_server.html:94 #: bookwyrm/templates/snippets/delete_readthrough_modal.html:17 #: bookwyrm/templates/snippets/goal_form.html:32 #: bookwyrm/templates/snippets/readthrough.html:76 @@ -203,92 +220,127 @@ msgstr "Enregistrer" msgid "Cancel" msgstr "Annuler" -#: bookwyrm/templates/book/book.html:121 +#: bookwyrm/templates/book/book.html:135 #, python-format -#| msgid "Editions of \"%(work_title)s\"" msgid "%(count)s editions" msgstr "%(count)s éditions" -#: bookwyrm/templates/book/book.html:129 +#: bookwyrm/templates/book/book.html:143 #, python-format -#| msgid "favorited your %(preview_name)s" msgid "This edition is on your %(shelf_name)s shelf." -msgstr "Cette édition est sur votre étagère %(shelf_name)s." +msgstr "" +"Cette édition est sur votre étagère %(shelf_name)s." -#: bookwyrm/templates/book/book.html:135 +#: bookwyrm/templates/book/book.html:149 #, python-format -#| msgid "replied to your %(preview_name)s" -msgid "A different edition of this book is on your %(shelf_name)s shelf." -msgstr "Une édition différente de ce livre existe sur votre étagère %(shelf_name)s." +msgid "" +"A different edition of this book is on your %(shelf_name)s shelf." +msgstr "" +"Une édition différente de ce livre existe sur " +"votre étagère %(shelf_name)s." -#: bookwyrm/templates/book/book.html:144 +#: bookwyrm/templates/book/book.html:158 msgid "Your reading activity" msgstr "Votre activité de lecture" -#: bookwyrm/templates/book/book.html:146 -#| msgid "Edit read dates" +#: bookwyrm/templates/book/book.html:160 msgid "Add read dates" msgstr "Ajouter des dates de lecture" -#: bookwyrm/templates/book/book.html:151 +#: bookwyrm/templates/book/book.html:165 msgid "You don't have any reading activity for this book." msgstr "Vous n’avez aucune activité de lecture pour ce livre" -#: bookwyrm/templates/book/book.html:158 +#: bookwyrm/templates/book/book.html:172 msgid "Create" msgstr "Créer" -#: bookwyrm/templates/book/book.html:180 +#: bookwyrm/templates/book/book.html:194 msgid "Subjects" msgstr "Sujets" -#: bookwyrm/templates/book/book.html:191 +#: bookwyrm/templates/book/book.html:206 msgid "Places" msgstr "Lieux" -#: bookwyrm/templates/book/book.html:202 bookwyrm/templates/layout.html:64 +#: bookwyrm/templates/book/book.html:217 bookwyrm/templates/layout.html:65 #: bookwyrm/templates/lists/lists.html:5 bookwyrm/templates/lists/lists.html:12 #: bookwyrm/templates/search_results.html:91 #: bookwyrm/templates/user/user_layout.html:62 msgid "Lists" msgstr "Listes" -#: bookwyrm/templates/book/book.html:213 -#| msgid "Go to list" +#: bookwyrm/templates/book/book.html:228 msgid "Add to list" msgstr "Ajouter à la liste" -#: bookwyrm/templates/book/book.html:223 +#: bookwyrm/templates/book/book.html:238 #: bookwyrm/templates/book/cover_modal.html:31 -#: bookwyrm/templates/lists/list.html:90 +#: bookwyrm/templates/lists/list.html:133 msgid "Add" msgstr "Ajouter" -#: bookwyrm/templates/book/book.html:251 +#: bookwyrm/templates/book/book.html:254 +#, fuzzy +#| msgid "Review" +msgid "Reviews" +msgstr "Critique" + +#: bookwyrm/templates/book/book.html:259 +#, fuzzy +#| msgid "Your shelves" +msgid "Your reviews" +msgstr "Vos étagères" + +#: bookwyrm/templates/book/book.html:265 +#, fuzzy +#| msgid "Your Account" +msgid "Your comments" +msgstr "Votre compte" + +#: bookwyrm/templates/book/book.html:271 +#, fuzzy +#| msgid "Your books" +msgid "Your quotes" +msgstr "Vos livres" + +#: bookwyrm/templates/book/book.html:305 msgid "rated it" msgstr "l’a noté" +#: bookwyrm/templates/book/book_identifiers.html:8 +msgid "ISBN:" +msgstr "ISBN :" + +#: bookwyrm/templates/book/book_identifiers.html:15 +#: bookwyrm/templates/book/edit_book.html:226 +msgid "OCLC Number:" +msgstr "Numéro OCLC :" + +#: bookwyrm/templates/book/book_identifiers.html:22 +#: bookwyrm/templates/book/edit_book.html:230 +msgid "ASIN:" +msgstr "ASIN :" + #: bookwyrm/templates/book/cover_modal.html:17 -#: bookwyrm/templates/book/edit_book.html:169 -#| msgid "Add cover" +#: bookwyrm/templates/book/edit_book.html:178 msgid "Upload cover:" msgstr "Charger une couverture :" #: bookwyrm/templates/book/cover_modal.html:23 -#: bookwyrm/templates/book/edit_book.html:175 +#: bookwyrm/templates/book/edit_book.html:184 msgid "Load cover from url:" msgstr "Charger la couverture depuis une URL :" #: bookwyrm/templates/book/edit_book.html:5 #: bookwyrm/templates/book/edit_book.html:11 #, python-format -#| msgid "Finish \"%(book_title)s\"" msgid "Edit \"%(book_title)s\"" msgstr "Modifier « %(book_title)s »" #: bookwyrm/templates/book/edit_book.html:5 #: bookwyrm/templates/book/edit_book.html:13 -#| msgid "Add Books" msgid "Add Book" msgstr "Ajouter un livre" @@ -318,7 +370,6 @@ msgstr "Est‑ce que l’auteur ou l’autrice « %(name)s » existe déjà #: bookwyrm/templates/book/edit_book.html:52 #, python-format -#| msgid "Start \"%(book_title)s\"" msgid "Author of %(book_title)s" msgstr "Commencer « %(book_title)s »" @@ -354,97 +405,92 @@ msgstr "Retour" msgid "Metadata" msgstr "Métadonnées" -#: bookwyrm/templates/book/edit_book.html:91 +#: bookwyrm/templates/book/edit_book.html:92 msgid "Title:" msgstr "Titre :" -#: bookwyrm/templates/book/edit_book.html:96 +#: bookwyrm/templates/book/edit_book.html:100 msgid "Subtitle:" msgstr "Sous‑titre :" -#: bookwyrm/templates/book/edit_book.html:106 +#: bookwyrm/templates/book/edit_book.html:113 msgid "Series:" msgstr "Série :" -#: bookwyrm/templates/book/edit_book.html:111 +#: bookwyrm/templates/book/edit_book.html:120 msgid "Series number:" msgstr "Numéro dans la série :" -#: bookwyrm/templates/book/edit_book.html:117 -#| msgid "Published" +#: bookwyrm/templates/book/edit_book.html:126 msgid "Publisher:" msgstr "Éditeur :" -#: bookwyrm/templates/book/edit_book.html:119 +#: bookwyrm/templates/book/edit_book.html:128 msgid "Separate multiple publishers with commas." msgstr "Séparez plusieurs éditeurs par une virgule." -#: bookwyrm/templates/book/edit_book.html:126 +#: bookwyrm/templates/book/edit_book.html:135 msgid "First published date:" msgstr "Première date de publication :" -#: bookwyrm/templates/book/edit_book.html:134 +#: bookwyrm/templates/book/edit_book.html:143 msgid "Published date:" msgstr "Date de publication :" -#: bookwyrm/templates/book/edit_book.html:143 -#| msgid "Author" +#: bookwyrm/templates/book/edit_book.html:152 msgid "Authors" msgstr "Auteurs ou autrices" -#: bookwyrm/templates/book/edit_book.html:149 +#: bookwyrm/templates/book/edit_book.html:158 #, python-format -#| msgid "favorited your %(preview_name)s" msgid "Remove %(name)s" msgstr "Supprimer %(name)s" -#: bookwyrm/templates/book/edit_book.html:154 -#| msgid "Edit Author" +#: bookwyrm/templates/book/edit_book.html:163 msgid "Add Authors:" msgstr "Ajouter des auteurs ou autrices :" -#: bookwyrm/templates/book/edit_book.html:155 +#: bookwyrm/templates/book/edit_book.html:164 msgid "John Doe, Jane Smith" msgstr "Claude Dupont, Dominique Durand" -#: bookwyrm/templates/book/edit_book.html:161 -#: bookwyrm/templates/user/shelf.html:75 +#: bookwyrm/templates/book/edit_book.html:170 +#: bookwyrm/templates/user/shelf.html:74 msgid "Cover" msgstr "Couverture" -#: bookwyrm/templates/book/edit_book.html:188 +#: bookwyrm/templates/book/edit_book.html:197 msgid "Physical Properties" msgstr "Propriétés physiques" -#: bookwyrm/templates/book/edit_book.html:189 +#: bookwyrm/templates/book/edit_book.html:198 #: bookwyrm/templates/book/format_filter.html:5 msgid "Format:" msgstr "Format :" -#: bookwyrm/templates/book/edit_book.html:197 +#: bookwyrm/templates/book/edit_book.html:206 msgid "Pages:" msgstr "Pages :" -#: bookwyrm/templates/book/edit_book.html:204 +#: bookwyrm/templates/book/edit_book.html:213 msgid "Book Identifiers" msgstr "Identifiants du livre" -#: bookwyrm/templates/book/edit_book.html:205 +#: bookwyrm/templates/book/edit_book.html:214 msgid "ISBN 13:" msgstr "ISBN 13 :" -#: bookwyrm/templates/book/edit_book.html:209 +#: bookwyrm/templates/book/edit_book.html:218 msgid "ISBN 10:" msgstr "ISBN 10 :" -#: bookwyrm/templates/book/edit_book.html:213 +#: bookwyrm/templates/book/edit_book.html:222 #: bookwyrm/templates/edit_author.html:59 msgid "Openlibrary key:" msgstr "Clé Openlibrary :" #: bookwyrm/templates/book/editions.html:5 #, python-format -#| msgid "Finish \"%(book_title)s\"" msgid "Editions of %(book_title)s" msgstr "Éditions de %(book_title)s" @@ -462,36 +508,37 @@ msgstr "Tou(te)s" msgid "Language:" msgstr "Langue :" -#: bookwyrm/templates/book/publisher_info.html:6 +#: bookwyrm/templates/book/publisher_info.html:22 +#, python-format +msgid "%(format)s" +msgstr "%(format)s" + +#: bookwyrm/templates/book/publisher_info.html:24 #, python-format -#| msgid "of %(book.pages)s pages" msgid "%(format)s, %(pages)s pages" msgstr "%(format)s, %(pages)s pages" -#: bookwyrm/templates/book/publisher_info.html:8 +#: bookwyrm/templates/book/publisher_info.html:26 #, python-format -#| msgid "of %(book.pages)s pages" msgid "%(pages)s pages" msgstr "%(pages)s pages" -#: bookwyrm/templates/book/publisher_info.html:13 +#: bookwyrm/templates/book/publisher_info.html:38 #, python-format -#| msgid "of %(book.pages)s pages" msgid "%(languages)s language" msgstr "%(languages)s langues" -#: bookwyrm/templates/book/publisher_info.html:18 +#: bookwyrm/templates/book/publisher_info.html:64 #, python-format msgid "Published %(date)s by %(publisher)s." msgstr "Publié %(date)s par %(publisher)s." -#: bookwyrm/templates/book/publisher_info.html:20 +#: bookwyrm/templates/book/publisher_info.html:66 #, python-format -#| msgid "Published date:" msgid "Published %(date)s" msgstr "Publié %(date)s" -#: bookwyrm/templates/book/publisher_info.html:22 +#: bookwyrm/templates/book/publisher_info.html:68 #, python-format msgid "Published by %(publisher)s." msgstr "Publié par %(publisher)s." @@ -501,45 +548,45 @@ msgstr "Publié par %(publisher)s." #: bookwyrm/templates/feed/feed_layout.html:70 #: bookwyrm/templates/get_started/layout.html:19 #: bookwyrm/templates/get_started/layout.html:52 -#| msgid "Closed" msgid "Close" msgstr "Fermer" #: bookwyrm/templates/compose.html:5 bookwyrm/templates/compose.html:8 -#| msgid "Boost status" msgid "Compose status" msgstr "Rédiger un statut" #: bookwyrm/templates/directory/community_filter.html:5 -#| msgid "Comment" msgid "Community" msgstr "Communauté" #: bookwyrm/templates/directory/community_filter.html:8 -#| msgid "Blocked users" msgid "Local users" msgstr "Comptes locaux" #: bookwyrm/templates/directory/community_filter.html:12 -#| msgid "Federated" msgid "Federated community" msgstr "Communauté fédérée" #: bookwyrm/templates/directory/directory.html:6 #: bookwyrm/templates/directory/directory.html:11 -#: bookwyrm/templates/layout.html:92 +#: bookwyrm/templates/layout.html:93 msgid "Directory" msgstr "Répertoire" #: bookwyrm/templates/directory/directory.html:19 msgid "Make your profile discoverable to other BookWyrm users." -msgstr "Autoriser d’autres utilisateurs ou utilisatrices de BookWyrm à découvrir votre profil." +msgstr "" +"Autoriser d’autres utilisateurs ou utilisatrices de BookWyrm à découvrir " +"votre profil." #: bookwyrm/templates/directory/directory.html:26 #, python-format -#| msgid "You can set or change your reading goal any time from your profile page" -msgid "You can opt-out at any time in your profile settings." -msgstr "Vous pouvez décider de ne plus y figurer à n’importe quel moment depuis vos paramètres de profil." +msgid "" +"You can opt-out at any time in your profile settings." +msgstr "" +"Vous pouvez décider de ne plus y figurer à n’importe quel moment depuis vos " +"paramètres de profil." #: bookwyrm/templates/directory/directory.html:31 #: bookwyrm/templates/snippets/goal_card.html:22 @@ -547,14 +594,12 @@ msgid "Dismiss message" msgstr "Rejeter le message" #: bookwyrm/templates/directory/directory.html:71 -#| msgid "followed you" msgid "follower you follow" msgid_plural "followers you follow" msgstr[0] "compte auquel vous êtes abonné(e)" msgstr[1] "comptes auxquels vous êtes abonné(e)" #: bookwyrm/templates/directory/directory.html:78 -#| msgid "Your shelves" msgid "book on your shelves" msgid_plural "books on your shelves" msgstr[0] "livre sur vos étagères" @@ -573,7 +618,6 @@ msgid "Order by" msgstr "Trier par" #: bookwyrm/templates/directory/sort_filter.html:8 -#| msgid "Suggest" msgid "Suggested" msgstr "Suggéré" @@ -582,12 +626,10 @@ msgid "Recently active" msgstr "Actif récemment" #: bookwyrm/templates/directory/user_type_filter.html:5 -#| msgid "User Activity" msgid "User type" msgstr "Type de compte" #: bookwyrm/templates/directory/user_type_filter.html:8 -#| msgid "Blocked users" msgid "BookWyrm users" msgstr "Comptes BookWyrm" @@ -597,7 +639,6 @@ msgstr "Tous les comptes connus" #: bookwyrm/templates/discover/about.html:7 #, python-format -#| msgid "Join %(name)s" msgid "About %(site_name)s" msgstr "À propos de %(site_name)s" @@ -658,7 +699,7 @@ msgid "Email address:" msgstr "Adresse email :" #: bookwyrm/templates/discover/landing_layout.html:70 -#: bookwyrm/templates/moderation/report_modal.html:31 +#: bookwyrm/templates/moderation/report_modal.html:33 msgid "Submit" msgstr "Valider" @@ -667,7 +708,6 @@ msgid "Your Account" msgstr "Votre compte" #: bookwyrm/templates/edit_author.html:5 -#| msgid "Edit Author" msgid "Edit Author:" msgstr "Modifier l’auteur ou l’autrice :" @@ -712,8 +752,12 @@ msgstr "Bien le bonjour," #: bookwyrm/templates/email/html_layout.html:21 #, python-format -msgid "BookWyrm hosted on %(site_name)s" -msgstr "BookWyrm, hébergé par %(site_name)s" +msgid "" +"BookWyrm hosted on " +"%(site_name)s" +msgstr "" +"BookWyrm, hébergé par %(site_name)s" #: bookwyrm/templates/email/html_layout.html:23 msgid "Email preference" @@ -722,7 +766,6 @@ msgstr "Paramètres d’email" #: bookwyrm/templates/email/invite/html_content.html:6 #: bookwyrm/templates/email/invite/subject.html:2 #, python-format -#| msgid "Join %(name)s" msgid "You're invited to join %(site_name)s!" msgstr "Vous avez reçu une invitation à rejoindre %(site_name)s !" @@ -732,24 +775,36 @@ msgstr "S’enregistrer maintenant" #: bookwyrm/templates/email/invite/html_content.html:15 #, python-format -msgid "Learn more about this instance." -msgstr "En savoir plus sur cette instance." +msgid "" +"Learn more about this instance." +msgstr "" +"En savoir plus sur cette " +"instance." #: bookwyrm/templates/email/invite/text_content.html:4 #, python-format -msgid "You're invited to join %(site_name)s! Click the link below to create an account." -msgstr "Vous avez reçu une invitation à rejoindre %(site_name)s ! Cliquez le lien suivant pour créer un compte." +msgid "" +"You're invited to join %(site_name)s! Click the link below to create an " +"account." +msgstr "" +"Vous avez reçu une invitation à rejoindre %(site_name)s ! Cliquez le lien " +"suivant pour créer un compte." #: bookwyrm/templates/email/invite/text_content.html:8 -#| msgid "More about this site" msgid "Learn more about this instance:" msgstr "En savoir plus sur cete instance :" #: bookwyrm/templates/email/password_reset/html_content.html:6 #: bookwyrm/templates/email/password_reset/text_content.html:4 #, python-format -msgid "You requested to reset your %(site_name)s password. Click the link below to set a new password and log in to your account." -msgstr "Une demande de réinitialisation de votre mot de passe sur %(site_name)s a été initialisée. Cliquez le lien suivant pour définir un nouveau mot de passe et vous connecter à votre compte." +msgid "" +"You requested to reset your %(site_name)s password. Click the link below to " +"set a new password and log in to your account." +msgstr "" +"Une demande de réinitialisation de votre mot de passe sur %(site_name)s a été initialisée. Cliquez le lien suivant pour définir un nouveau mot de " +"passe et vous connecter à votre compte." #: bookwyrm/templates/email/password_reset/html_content.html:9 #: bookwyrm/templates/password_reset.html:4 @@ -761,12 +816,14 @@ msgstr "Changez le mot de passe" #: bookwyrm/templates/email/password_reset/html_content.html:13 #: bookwyrm/templates/email/password_reset/text_content.html:8 -msgid "If you didn't request to reset your password, you can ignore this email." -msgstr "Si vous n’avez pas demandé la réinitialisation de votre mot de passe, vous pouvez ignorer cet email." +msgid "" +"If you didn't request to reset your password, you can ignore this email." +msgstr "" +"Si vous n’avez pas demandé la réinitialisation de votre mot de passe, vous " +"pouvez ignorer cet email." #: bookwyrm/templates/email/password_reset/subject.html:2 #, python-format -#| msgid "Join %(name)s" msgid "Reset your %(site_name)s password" msgstr "Réinitialiser votre mot de passe sur %(site_name)s" @@ -776,7 +833,7 @@ msgid "Direct Messages with %(username)s" msgstr "Messages directs avec %(username)s" #: bookwyrm/templates/feed/direct_messages.html:10 -#: bookwyrm/templates/layout.html:87 +#: bookwyrm/templates/layout.html:88 msgid "Direct Messages" msgstr "Messages directs" @@ -793,12 +850,10 @@ msgid "Home Timeline" msgstr "Mon fil d’actualité" #: bookwyrm/templates/feed/feed.html:11 -#| msgid "%(tab_title)s Timeline" msgid "Local Timeline" msgstr "Fil d’actualité local" #: bookwyrm/templates/feed/feed.html:13 -#| msgid "Federated Servers" msgid "Federated Timeline" msgstr "Fil d’actualité des instances fédérées" @@ -811,17 +866,21 @@ msgid "Local" msgstr "Local" #: bookwyrm/templates/feed/feed.html:25 +#: bookwyrm/templates/settings/edit_server.html:40 msgid "Federated" msgstr "Fédéré" #: bookwyrm/templates/feed/feed.html:33 #, python-format msgid "load 0 unread status(es)" -msgstr "charger le(s) 0 statut(s) non lu(s)" +msgstr "" +"charger le(s) 0 statut(s) non lu(s)" #: bookwyrm/templates/feed/feed.html:48 -msgid "There aren't any activities right now! Try following a user to get started" -msgstr "Aucune activité pour l’instant ! Abonnez‑vous à quelqu’un pour commencer" +msgid "" +"There aren't any activities right now! Try following a user to get started" +msgstr "" +"Aucune activité pour l’instant ! Abonnez‑vous à quelqu’un pour commencer" #: bookwyrm/templates/feed/feed.html:56 #: bookwyrm/templates/get_started/users.html:6 @@ -829,29 +888,27 @@ msgid "Who to follow" msgstr "À qui s’abonner" #: bookwyrm/templates/feed/feed_layout.html:5 -#| msgid "Updated:" msgid "Updates" msgstr "Mises à jour" #: bookwyrm/templates/feed/feed_layout.html:11 -#: bookwyrm/templates/layout.html:58 +#: bookwyrm/templates/layout.html:59 #: bookwyrm/templates/user/books_header.html:3 msgid "Your books" msgstr "Vos livres" #: bookwyrm/templates/feed/feed_layout.html:13 -msgid "There are no books here right now! Try searching for a book to get started" +msgid "" +"There are no books here right now! Try searching for a book to get started" msgstr "Aucun livre ici pour l’instant ! Cherchez un livre pour commencer" #: bookwyrm/templates/feed/feed_layout.html:24 #: bookwyrm/templates/user/shelf.html:28 -#| msgid "Read" msgid "To Read" msgstr "À lire" #: bookwyrm/templates/feed/feed_layout.html:25 #: bookwyrm/templates/user/shelf.html:28 -#| msgid "Started reading" msgid "Currently Reading" msgstr "En train de lire" @@ -883,17 +940,15 @@ msgstr[1] "%(shared_books)s livres sur vos étagères" #: bookwyrm/templates/get_started/book_preview.html:6 #, python-format -#| msgid "Want to Read \"%(book_title)s\"" msgid "Have you read %(book_title)s?" msgstr "Avez‑vous lu « %(book_title)s » ?" #: bookwyrm/templates/get_started/books.html:6 -#| msgid "Started reading" msgid "What are you reading?" msgstr "Que lisez‑vous ?" #: bookwyrm/templates/get_started/books.html:9 -#: bookwyrm/templates/lists/list.html:58 +#: bookwyrm/templates/lists/list.html:101 msgid "Search for a book" msgstr "Chercher un livre" @@ -907,30 +962,30 @@ msgstr "Aucun livre trouvé pour « %(query)s »" #: bookwyrm/templates/get_started/books.html:11 #, python-format msgid "You can add books when you start using %(site_name)s." -msgstr "Vous pourrez ajouter des livres lorsque vous commencerez à utiliser %(site_name)s." +msgstr "" +"Vous pourrez ajouter des livres lorsque vous commencerez à utiliser " +"%(site_name)s." #: bookwyrm/templates/get_started/books.html:16 #: bookwyrm/templates/get_started/books.html:17 #: bookwyrm/templates/get_started/users.html:18 #: bookwyrm/templates/get_started/users.html:19 -#: bookwyrm/templates/layout.html:37 bookwyrm/templates/layout.html:38 -#: bookwyrm/templates/lists/list.html:62 +#: bookwyrm/templates/layout.html:38 bookwyrm/templates/layout.html:39 +#: bookwyrm/templates/lists/list.html:105 msgid "Search" msgstr "Chercher" #: bookwyrm/templates/get_started/books.html:26 -#| msgid "Suggest Books" msgid "Suggested Books" msgstr "Suggérer des livres" #: bookwyrm/templates/get_started/books.html:41 #, python-format -#| msgid "Join %(name)s" msgid "Popular on %(site_name)s" msgstr "Populaire sur %(site_name)s" #: bookwyrm/templates/get_started/books.html:51 -#: bookwyrm/templates/lists/list.html:75 +#: bookwyrm/templates/lists/list.html:118 msgid "No books found" msgstr "Aucun livre trouvé" @@ -941,7 +996,6 @@ msgstr "Enregistrer & continuer" #: bookwyrm/templates/get_started/layout.html:14 #, python-format -#| msgid "Join %(name)s" msgid "Welcome to %(site_name)s!" msgstr "Bienvenu(e) sur %(site_name)s !" @@ -951,17 +1005,14 @@ msgstr "Voici quelques étapes pour commencer votre profil." #: bookwyrm/templates/get_started/layout.html:30 #: bookwyrm/templates/get_started/profile.html:6 -#| msgid "User Profile" msgid "Create your profile" msgstr "Créez votre profil" #: bookwyrm/templates/get_started/layout.html:34 -#| msgid "Add Books" msgid "Add books" msgstr "Ajoutez des livres" #: bookwyrm/templates/get_started/layout.html:38 -#| msgid "Friendly" msgid "Find friends" msgstr "Établissez des contacts" @@ -970,7 +1021,6 @@ msgid "Skip this step" msgstr "Passer cette étape" #: bookwyrm/templates/get_started/layout.html:48 -#| msgid "Finished" msgid "Finish" msgstr "Terminer" @@ -1004,11 +1054,14 @@ msgid "Show this account in suggested users:" msgstr "Afficher ce compte dans ceux suggérés :" #: bookwyrm/templates/get_started/profile.html:52 -msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users." -msgstr "Votre compte sera listé dans le répertoire et pourra être recommandé à d’autres utilisateurs ou utilisatrices de BookWyrm." +msgid "" +"Your account will show up in the directory, and may be recommended to other " +"BookWyrm users." +msgstr "" +"Votre compte sera listé dans le répertoire et pourra être recommandé à " +"d’autres utilisateurs ou utilisatrices de BookWyrm." #: bookwyrm/templates/get_started/users.html:11 -#| msgid "Search for a book or user" msgid "Search for a user" msgstr "Chercher un compte" @@ -1024,15 +1077,18 @@ msgid "%(year)s Reading Progress" msgstr "Progression de lecture pour %(year)s" #: bookwyrm/templates/goal.html:11 -#| msgid "Edit Book" msgid "Edit Goal" msgstr "Modifier le défi" #: bookwyrm/templates/goal.html:30 #: bookwyrm/templates/snippets/goal_card.html:13 #, python-format -msgid "Set a goal for how many books you'll finish reading in %(year)s, and track your progress throughout the year." -msgstr "Définissez un nombre de livre à lire comme objectif pour %(year)s, et suivezvotre progression au fil de l’année." +msgid "" +"Set a goal for how many books you'll finish reading in %(year)s, and track " +"your progress throughout the year." +msgstr "" +"Définissez un nombre de livre à lire comme objectif pour %(year)s, et " +"suivezvotre progression au fil de l’année." #: bookwyrm/templates/goal.html:39 #, python-format @@ -1041,23 +1097,20 @@ msgstr "%(name)s n’a aucun défi lecture pour %(year)s." #: bookwyrm/templates/goal.html:51 #, python-format -#| msgid "Your books" msgid "Your %(year)s Books" msgstr "Vos livres en %(year)s" #: bookwyrm/templates/goal.html:53 #, python-format -#| msgid "%(username)s has no followers" msgid "%(username)s's %(year)s Books" msgstr "Livres de %(username)s en %(year)s" #: bookwyrm/templates/import.html:5 bookwyrm/templates/import.html:9 -#: bookwyrm/templates/layout.html:97 +#: bookwyrm/templates/layout.html:98 msgid "Import Books" msgstr "Importer des livres" #: bookwyrm/templates/import.html:16 -#| msgid "Data source" msgid "Data source:" msgstr "Source de données :" @@ -1074,6 +1127,7 @@ msgid "Privacy setting for imported reviews:" msgstr "Confidentialité des critiques importées :" #: bookwyrm/templates/import.html:48 +#: bookwyrm/templates/settings/server_blocklist.html:64 msgid "Import" msgstr "Importer" @@ -1116,8 +1170,12 @@ msgstr "Items non importés" #: bookwyrm/templates/import_status.html:44 #, python-format -msgid "Jump to the bottom of the list to select the %(failed_count)s items which failed to import." -msgstr "Sauter en bas de liste pour sélectionner les %(failed_count)s items n’ayant pu être importés." +msgid "" +"Jump to the bottom of the list to select the %(failed_count)s items which " +"failed to import." +msgstr "" +"Sauter en bas de liste pour sélectionner les %(failed_count)s items n’ayant " +"pu être importés." #: bookwyrm/templates/import_status.html:79 msgid "Select all" @@ -1138,12 +1196,12 @@ msgstr "Livre" #: bookwyrm/templates/import_status.html:115 #: bookwyrm/templates/snippets/create_status_form.html:10 -#: bookwyrm/templates/user/shelf.html:76 +#: bookwyrm/templates/user/shelf.html:75 bookwyrm/templates/user/shelf.html:93 msgid "Title" msgstr "Titre" #: bookwyrm/templates/import_status.html:118 -#: bookwyrm/templates/user/shelf.html:77 +#: bookwyrm/templates/user/shelf.html:76 bookwyrm/templates/user/shelf.html:96 msgid "Author" msgstr "Auteur ou autrice" @@ -1180,100 +1238,104 @@ msgstr "Résultats de recherche pour « %(query)s »" msgid "Matching Books" msgstr "Livres correspondants" -#: bookwyrm/templates/layout.html:33 +#: bookwyrm/templates/layout.html:34 msgid "Search for a book or user" msgstr "Chercher un livre ou un compte" -#: bookwyrm/templates/layout.html:47 bookwyrm/templates/layout.html:48 +#: bookwyrm/templates/layout.html:48 bookwyrm/templates/layout.html:49 msgid "Main navigation menu" msgstr "Menu de navigation principal " -#: bookwyrm/templates/layout.html:61 +#: bookwyrm/templates/layout.html:62 msgid "Feed" msgstr "Fil d’actualité" -#: bookwyrm/templates/layout.html:102 -#| msgid "Instance Settings" +#: bookwyrm/templates/layout.html:103 msgid "Settings" msgstr "Paramètres" -#: bookwyrm/templates/layout.html:111 -#: bookwyrm/templates/settings/admin_layout.html:24 +#: bookwyrm/templates/layout.html:112 +#: bookwyrm/templates/settings/admin_layout.html:31 #: bookwyrm/templates/settings/manage_invite_requests.html:15 #: bookwyrm/templates/settings/manage_invites.html:3 #: bookwyrm/templates/settings/manage_invites.html:15 msgid "Invites" msgstr "Invitations" -#: bookwyrm/templates/layout.html:118 +#: bookwyrm/templates/layout.html:119 msgid "Admin" msgstr "Admin" -#: bookwyrm/templates/layout.html:125 +#: bookwyrm/templates/layout.html:126 msgid "Log out" msgstr "Se déconnecter" -#: bookwyrm/templates/layout.html:133 bookwyrm/templates/layout.html:134 +#: bookwyrm/templates/layout.html:134 bookwyrm/templates/layout.html:135 #: bookwyrm/templates/notifications.html:6 #: bookwyrm/templates/notifications.html:10 msgid "Notifications" msgstr "Notifications" -#: bookwyrm/templates/layout.html:151 bookwyrm/templates/layout.html:155 +#: bookwyrm/templates/layout.html:152 bookwyrm/templates/layout.html:156 #: bookwyrm/templates/login.html:17 #: bookwyrm/templates/snippets/register_form.html:4 msgid "Username:" msgstr "Nom du compte :" -#: bookwyrm/templates/layout.html:156 -#| msgid "Password:" +#: bookwyrm/templates/layout.html:157 msgid "password" msgstr "Mot de passe" -#: bookwyrm/templates/layout.html:157 bookwyrm/templates/login.html:36 +#: bookwyrm/templates/layout.html:158 bookwyrm/templates/login.html:36 msgid "Forgot your password?" msgstr "Mot de passe oublié ?" -#: bookwyrm/templates/layout.html:160 bookwyrm/templates/login.html:10 +#: bookwyrm/templates/layout.html:161 bookwyrm/templates/login.html:10 #: bookwyrm/templates/login.html:33 msgid "Log in" msgstr "Se connecter" -#: bookwyrm/templates/layout.html:168 +#: bookwyrm/templates/layout.html:169 msgid "Join" msgstr "Rejoindre" -#: bookwyrm/templates/layout.html:191 +#: bookwyrm/templates/layout.html:195 msgid "About this server" msgstr "À propos de ce serveur" -#: bookwyrm/templates/layout.html:195 +#: bookwyrm/templates/layout.html:199 msgid "Contact site admin" msgstr "Contacter l’administrateur du site" -#: bookwyrm/templates/layout.html:202 -#, python-format -msgid "Support %(site_name)s on %(support_title)s" -msgstr "Soutenez %(site_name)s avec %(support_title)s" - #: bookwyrm/templates/layout.html:206 -msgid "BookWyrm is open source software. You can contribute or report issues on GitHub." -msgstr "BookWyrm est un logiciel libre. Vous pouvez contribuer ou faire des rapports de bogues via GitHub." +#, python-format +msgid "" +"Support %(site_name)s on " +"%(support_title)s" +msgstr "" +"Soutenez %(site_name)s avec " +"%(support_title)s" + +#: bookwyrm/templates/layout.html:210 +msgid "" +"BookWyrm is open source software. You can contribute or report issues on GitHub." +msgstr "" +"BookWyrm est un logiciel libre. Vous pouvez contribuer ou faire des rapports " +"de bogues via GitHub." #: bookwyrm/templates/lists/create_form.html:5 -#: bookwyrm/templates/lists/lists.html:19 +#: bookwyrm/templates/lists/lists.html:20 msgid "Create List" msgstr "Créer une liste" #: bookwyrm/templates/lists/created_text.html:5 #, python-format -#| msgid "favorited your %(preview_name)s" msgid "Created and curated by %(username)s" msgstr "Créée et modérée par %(username)s" #: bookwyrm/templates/lists/created_text.html:7 #, python-format -#| msgid "favorited your %(preview_name)s" msgid "Created by %(username)s" msgstr "Créée par %(username)s" @@ -1327,7 +1389,7 @@ msgid "Anyone can suggest books, subject to your approval" msgstr "N’importe qui peut suggérer des livres, soumis à votre approbation" #: bookwyrm/templates/lists/form.html:31 -#: bookwyrm/templates/moderation/reports.html:24 +#: bookwyrm/templates/moderation/reports.html:25 msgid "Open" msgstr "Ouverte" @@ -1335,46 +1397,83 @@ msgstr "Ouverte" msgid "Anyone can add books to this list" msgstr "N’importe qui peut suggérer des livres" -#: bookwyrm/templates/lists/list.html:17 +#: bookwyrm/templates/lists/list.html:19 +msgid "You successfully suggested a book for this list!" +msgstr "" + +#: bookwyrm/templates/lists/list.html:21 +#, fuzzy +#| msgid "Anyone can add books to this list" +msgid "You successfully added a book to this list!" +msgstr "N’importe qui peut suggérer des livres" + +#: bookwyrm/templates/lists/list.html:27 msgid "This list is currently empty" msgstr "Cette liste est vide actuellement" -#: bookwyrm/templates/lists/list.html:35 +#: bookwyrm/templates/lists/list.html:46 #, python-format -#| msgid "favorited your %(preview_name)s" msgid "Added by %(username)s" msgstr "Ajoutée par %(username)s" -#: bookwyrm/templates/lists/list.html:41 +#: bookwyrm/templates/lists/list.html:58 +#, fuzzy +#| msgid "Sent" +msgid "Set" +msgstr "Envoyé(e)s" + +#: bookwyrm/templates/lists/list.html:61 +#, fuzzy +#| msgid "List curation:" +msgid "List position" +msgstr "Modération de la liste :" + +#: bookwyrm/templates/lists/list.html:67 #: bookwyrm/templates/snippets/shelf_selector.html:26 msgid "Remove" msgstr "Supprimer" -#: bookwyrm/templates/lists/list.html:54 +#: bookwyrm/templates/lists/list.html:80 bookwyrm/templates/lists/list.html:92 +#, fuzzy +#| msgid "Your Lists" +msgid "Sort List" +msgstr "Vos listes" + +#: bookwyrm/templates/lists/list.html:86 +#, fuzzy +#| msgid "Directory" +msgid "Direction" +msgstr "Répertoire" + +#: bookwyrm/templates/lists/list.html:97 msgid "Add Books" msgstr "Ajouter des livres" -#: bookwyrm/templates/lists/list.html:54 +#: bookwyrm/templates/lists/list.html:97 msgid "Suggest Books" msgstr "Suggérer des livres" -#: bookwyrm/templates/lists/list.html:63 +#: bookwyrm/templates/lists/list.html:106 msgid "search" msgstr "Chercher" -#: bookwyrm/templates/lists/list.html:69 +#: bookwyrm/templates/lists/list.html:112 msgid "Clear search" msgstr "Vider la requête" -#: bookwyrm/templates/lists/list.html:74 +#: bookwyrm/templates/lists/list.html:117 #, python-format msgid "No books found matching the query \"%(query)s\"" msgstr "Aucun livre trouvé pour la requête « %(query)s »" -#: bookwyrm/templates/lists/list.html:90 +#: bookwyrm/templates/lists/list.html:133 msgid "Suggest" msgstr "Suggérer" +#: bookwyrm/templates/lists/lists.html:14 bookwyrm/templates/user/lists.html:9 +msgid "Your Lists" +msgstr "Vos listes" + #: bookwyrm/templates/login.html:4 msgid "Login" msgstr "Connexion" @@ -1392,77 +1491,54 @@ msgstr "Contacter un administrateur pour obtenir une invitation" msgid "More about this site" msgstr "En savoir plus sur ce site" -#: bookwyrm/templates/moderation/report.html:5 #: bookwyrm/templates/moderation/report.html:6 +#: bookwyrm/templates/moderation/report.html:7 #: bookwyrm/templates/moderation/report_preview.html:6 #, python-format msgid "Report #%(report_id)s: %(username)s" msgstr "Signalement #%(report_id)s : %(username)s" -#: bookwyrm/templates/moderation/report.html:10 +#: bookwyrm/templates/moderation/report.html:11 msgid "Back to reports" msgstr "Retour aux signalements" -#: bookwyrm/templates/moderation/report.html:18 -#| msgid "Notifications" -msgid "Actions" -msgstr "Actions" - -#: bookwyrm/templates/moderation/report.html:19 -#| msgid "User Profile" -msgid "View user profile" -msgstr "Voir le profil" - -#: bookwyrm/templates/moderation/report.html:22 -#: bookwyrm/templates/snippets/status/status_options.html:35 -#: bookwyrm/templates/snippets/user_options.html:13 -msgid "Send direct message" -msgstr "Envoyer un message direct" - -#: bookwyrm/templates/moderation/report.html:27 -msgid "Deactivate user" -msgstr "Désactiver le compte" - -#: bookwyrm/templates/moderation/report.html:29 -msgid "Reactivate user" -msgstr "Réactiver le compte" - -#: bookwyrm/templates/moderation/report.html:36 +#: bookwyrm/templates/moderation/report.html:23 msgid "Moderator Comments" msgstr "Commentaires de l’équipe de modération" -#: bookwyrm/templates/moderation/report.html:54 +#: bookwyrm/templates/moderation/report.html:41 #: bookwyrm/templates/snippets/create_status.html:28 -#: bookwyrm/templates/snippets/create_status_form.html:44 +#: bookwyrm/templates/snippets/create_status_form.html:53 msgid "Comment" msgstr "Commentaire" -#: bookwyrm/templates/moderation/report.html:59 -#| msgid "Delete status" +#: bookwyrm/templates/moderation/report.html:46 msgid "Reported statuses" msgstr "Statuts signalés" -#: bookwyrm/templates/moderation/report.html:61 +#: bookwyrm/templates/moderation/report.html:48 msgid "No statuses reported" msgstr "Aucun statut signalé" -#: bookwyrm/templates/moderation/report.html:67 -msgid "Statuses has been deleted" +#: bookwyrm/templates/moderation/report.html:54 +#, fuzzy +#| msgid "Statuses has been deleted" +msgid "Status has been deleted" msgstr "Les statuts ont été supprimés" #: bookwyrm/templates/moderation/report_modal.html:6 #, python-format -#| msgid "Join %(name)s" msgid "Report @%(username)s" msgstr "Signaler @%(username)s" -#: bookwyrm/templates/moderation/report_modal.html:21 +#: bookwyrm/templates/moderation/report_modal.html:23 #, python-format msgid "This report will be sent to %(site_name)s's moderators for review." -msgstr "Ce signalement sera envoyé à l’équipe de modération de %(site_name)s pour traitement." +msgstr "" +"Ce signalement sera envoyé à l’équipe de modération de %(site_name)s pour " +"traitement." -#: bookwyrm/templates/moderation/report_modal.html:22 -#| msgid "More about this site" +#: bookwyrm/templates/moderation/report_modal.html:24 msgid "More info about this report:" msgstr "En savoir plus sur ce signalement :" @@ -1472,7 +1548,6 @@ msgstr "Aucune note fournie" #: bookwyrm/templates/moderation/report_preview.html:20 #, python-format -#| msgid "favorited your %(preview_name)s" msgid "Reported by %(username)s" msgstr "Signalé par %(username)s" @@ -1486,30 +1561,25 @@ msgstr "Résoudre" #: bookwyrm/templates/moderation/reports.html:6 #, python-format -#| msgid "Join %(name)s" msgid "Reports: %(server_name)s" msgstr "Signalements : %(server_name)s" #: bookwyrm/templates/moderation/reports.html:8 -#: bookwyrm/templates/moderation/reports.html:16 -#: bookwyrm/templates/settings/admin_layout.html:28 -#| msgid "Recent Imports" +#: bookwyrm/templates/moderation/reports.html:17 +#: bookwyrm/templates/settings/admin_layout.html:35 msgid "Reports" msgstr "Signalements" -#: bookwyrm/templates/moderation/reports.html:13 +#: bookwyrm/templates/moderation/reports.html:14 #, python-format -#| msgid "Join %(name)s" msgid "Reports: %(server_name)s" msgstr "Signalements: %(server_name)s" -#: bookwyrm/templates/moderation/reports.html:27 -#| msgid "Shelved" +#: bookwyrm/templates/moderation/reports.html:28 msgid "Resolved" msgstr "Résolus" -#: bookwyrm/templates/moderation/reports.html:34 -#| msgid "No books found" +#: bookwyrm/templates/moderation/reports.html:37 msgid "No reports found." msgstr "Aucun signalement trouvé." @@ -1519,18 +1589,30 @@ msgstr "Supprimer les notifications" #: bookwyrm/templates/notifications.html:53 #, python-format -msgid "favorited your review of %(book_title)s" -msgstr "a ajouté votre critique de %(book_title)s à ses favoris" +msgid "" +"favorited your review of %(book_title)s" +msgstr "" +"a ajouté votre critique de %(book_title)s à ses favoris" #: bookwyrm/templates/notifications.html:55 #, python-format -msgid "favorited your comment on %(book_title)s" -msgstr "a ajouté votre commentaire sur %(book_title)s à ses favoris" +msgid "" +"favorited your comment on %(book_title)s" +msgstr "" +"a ajouté votre commentaire sur " +"%(book_title)s à ses favoris" #: bookwyrm/templates/notifications.html:57 #, python-format -msgid "favorited your quote from %(book_title)s" -msgstr "a ajouté votre citation de %(book_title)s à ses favoris" +msgid "" +"favorited your quote from %(book_title)s" +msgstr "" +"a ajouté votre citation de %(book_title)s à ses favoris" #: bookwyrm/templates/notifications.html:59 #, python-format @@ -1539,18 +1621,30 @@ msgstr "a ajouté votre statut à ses favoris" #: bookwyrm/templates/notifications.html:64 #, python-format -msgid "mentioned you in a review of %(book_title)s" -msgstr "vous a mentionné dans sa critique de %(book_title)s" +msgid "" +"mentioned you in a review of " +"%(book_title)s" +msgstr "" +"vous a mentionné dans sa critique de " +"%(book_title)s" #: bookwyrm/templates/notifications.html:66 #, python-format -msgid "mentioned you in a comment on %(book_title)s" -msgstr "vous a mentionné dans son commentaire sur %(book_title)s" +msgid "" +"mentioned you in a comment on " +"%(book_title)s" +msgstr "" +"vous a mentionné dans son commentaire sur " +"%(book_title)s" #: bookwyrm/templates/notifications.html:68 #, python-format -msgid "mentioned you in a quote from %(book_title)s" -msgstr "vous a mentionné dans sa citation de %(book_title)s" +msgid "" +"mentioned you in a quote from " +"%(book_title)s" +msgstr "" +"vous a mentionné dans sa citation de " +"%(book_title)s" #: bookwyrm/templates/notifications.html:70 #, python-format @@ -1559,23 +1653,39 @@ msgstr "vous a mentionné dans son statut" #: bookwyrm/templates/notifications.html:75 #, python-format -msgid "replied to your review of %(book_title)s" -msgstr "a répondu à votre critique de %(book_title)s" +msgid "" +"replied to your review of %(book_title)s" +msgstr "" +"a répondu à votre critique de %(book_title)s" #: bookwyrm/templates/notifications.html:77 #, python-format -msgid "replied to your comment on %(book_title)s" -msgstr "a répondu à votre commentaire sur %(book_title)s" +msgid "" +"replied to your comment on %(book_title)s" +msgstr "" +"a répondu à votre commentaire sur %(book_title)s" #: bookwyrm/templates/notifications.html:79 #, python-format -msgid "replied to your quote from %(book_title)s" -msgstr "a répondu à votre citation de %(book_title)s" +msgid "" +"replied to your quote from %(book_title)s" +msgstr "" +"a répondu à votre citation de %(book_title)s" #: bookwyrm/templates/notifications.html:81 #, python-format -msgid "replied to your status" -msgstr "a répondu à votre statut" +msgid "" +"replied to your status" +msgstr "" +"a répondu à votre statut" #: bookwyrm/templates/notifications.html:85 msgid "followed you" @@ -1587,18 +1697,30 @@ msgstr "vous a envoyé une demande d’abonnement" #: bookwyrm/templates/notifications.html:94 #, python-format -msgid "boosted your review of %(book_title)s" -msgstr "a partagé votre critique de %(book_title)s" +msgid "" +"boosted your review of %(book_title)s" +msgstr "" +"a partagé votre critique de %(book_title)s" #: bookwyrm/templates/notifications.html:96 #, python-format -msgid "boosted your comment on%(book_title)s" -msgstr "a partagé votre commentaire sur %(book_title)s" +msgid "" +"boosted your comment on%(book_title)s" +msgstr "" +"a partagé votre commentaire sur " +"%(book_title)s" #: bookwyrm/templates/notifications.html:98 #, python-format -msgid "boosted your quote from %(book_title)s" -msgstr "a partagé votre citation de %(book_title)s" +msgid "" +"boosted your quote from %(book_title)s" +msgstr "" +"a partagé votre citation de %(book_title)s" #: bookwyrm/templates/notifications.html:100 #, python-format @@ -1607,13 +1729,21 @@ msgstr "a partagé votre statut" #: bookwyrm/templates/notifications.html:104 #, python-format -msgid " added %(book_title)s to your list \"%(list_name)s\"" -msgstr " a ajouté %(book_title)s à votre liste « %(list_name)s »" +msgid "" +" added %(book_title)s to your list " +"\"%(list_name)s\"" +msgstr "" +" a ajouté %(book_title)s à votre " +"liste « %(list_name)s »" #: bookwyrm/templates/notifications.html:106 #, python-format -msgid " suggested adding %(book_title)s to your list \"%(list_name)s\"" -msgstr " a suggégré l’ajout de %(book_title)s à votre liste « %(list_name)s »" +msgid "" +" suggested adding %(book_title)s to " +"your list \"%(list_name)s\"" +msgstr "" +" a suggégré l’ajout de %(book_title)s " +"à votre liste « %(list_name)s »" #: bookwyrm/templates/notifications.html:110 #, python-format @@ -1623,7 +1753,8 @@ msgstr "Votre importation est terminée." #: bookwyrm/templates/notifications.html:113 #, python-format msgid "A new report needs moderation." -msgstr "Un nouveau signalement a besoin d’être traité." +msgstr "" +"Un nouveau signalement a besoin d’être traité." #: bookwyrm/templates/notifications.html:139 msgid "You're all caught up!" @@ -1636,7 +1767,8 @@ msgstr "Confirmez le mot de passe :" #: bookwyrm/templates/password_reset_request.html:14 msgid "A link to reset your password will be sent to your email address" -msgstr "Un lien pour changer votre mot de passe sera envoyé à votre addresse email" +msgstr "" +"Un lien pour changer votre mot de passe sera envoyé à votre addresse email" #: bookwyrm/templates/password_reset_request.html:28 msgid "Reset password" @@ -1670,12 +1802,17 @@ msgstr "Modifier le profil" #: bookwyrm/templates/preferences/edit_user.html:46 msgid "Show set reading goal prompt in feed:" -msgstr "Afficher le message pour définir un défi lecture dans le fil d’actualité :" +msgstr "" +"Afficher le message pour définir un défi lecture dans le fil d’actualité :" #: bookwyrm/templates/preferences/edit_user.html:62 #, python-format -msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users." -msgstr "Votre compte sera listé dans le répertoire et pourra être recommandé à d’autres utilisateurs ou utilisatrices de BookWyrm." +msgid "" +"Your account will show up in the directory, and may " +"be recommended to other BookWyrm users." +msgstr "" +"Votre compte sera listé dans le répertoire et " +"pourra être recommandé à d’autres utilisateurs ou utilisatrices de BookWyrm." #: bookwyrm/templates/preferences/edit_user.html:65 msgid "Preferred Timezone: " @@ -1718,128 +1855,197 @@ msgstr "Aucune liste trouvée pour « %(query)s »" msgid "Administration" msgstr "Administration" -#: bookwyrm/templates/settings/admin_layout.html:15 +#: bookwyrm/templates/settings/admin_layout.html:22 msgid "Manage Users" msgstr "Gérer les comptes" -#: bookwyrm/templates/settings/admin_layout.html:19 -#: bookwyrm/templates/settings/user_admin.html:3 -#: bookwyrm/templates/settings/user_admin.html:10 +#: bookwyrm/templates/settings/admin_layout.html:26 +#: bookwyrm/templates/user_admin/user_admin.html:3 +#: bookwyrm/templates/user_admin/user_admin.html:10 msgid "Users" msgstr "Comptes" -#: bookwyrm/templates/settings/admin_layout.html:32 +#: bookwyrm/templates/settings/admin_layout.html:39 #: bookwyrm/templates/settings/federation.html:3 #: bookwyrm/templates/settings/federation.html:5 msgid "Federated Servers" msgstr "Serveurs fédérés" -#: bookwyrm/templates/settings/admin_layout.html:37 +#: bookwyrm/templates/settings/admin_layout.html:44 msgid "Instance Settings" msgstr "Paramètres de l’instance" -#: bookwyrm/templates/settings/admin_layout.html:41 +#: bookwyrm/templates/settings/admin_layout.html:48 #: bookwyrm/templates/settings/site.html:4 #: bookwyrm/templates/settings/site.html:6 -#| msgid "Instance Settings" msgid "Site Settings" msgstr "Paramètres du site" -#: bookwyrm/templates/settings/admin_layout.html:44 +#: bookwyrm/templates/settings/admin_layout.html:51 #: bookwyrm/templates/settings/site.html:13 msgid "Instance Info" msgstr "Information sur l’instance" -#: bookwyrm/templates/settings/admin_layout.html:45 +#: bookwyrm/templates/settings/admin_layout.html:52 #: bookwyrm/templates/settings/site.html:39 msgid "Images" msgstr "Images" -#: bookwyrm/templates/settings/admin_layout.html:46 +#: bookwyrm/templates/settings/admin_layout.html:53 #: bookwyrm/templates/settings/site.html:59 msgid "Footer Content" msgstr "Contenu du pied de page" -#: bookwyrm/templates/settings/admin_layout.html:47 +#: bookwyrm/templates/settings/admin_layout.html:54 #: bookwyrm/templates/settings/site.html:77 msgid "Registration" msgstr "Enregistrement" -#: bookwyrm/templates/settings/federated_server.html:7 +#: bookwyrm/templates/settings/edit_server.html:3 +#: bookwyrm/templates/settings/edit_server.html:6 +#: bookwyrm/templates/settings/edit_server.html:20 +#: bookwyrm/templates/settings/federation.html:9 +#: bookwyrm/templates/settings/federation.html:10 +#: bookwyrm/templates/settings/server_blocklist.html:3 +#: bookwyrm/templates/settings/server_blocklist.html:20 +#, fuzzy +#| msgid "Add cover" +msgid "Add server" +msgstr "Ajouter une couverture" + +#: bookwyrm/templates/settings/edit_server.html:7 +#: bookwyrm/templates/settings/federated_server.html:12 +#: bookwyrm/templates/settings/server_blocklist.html:7 msgid "Back to server list" msgstr "Retour à la liste des serveurs" -#: bookwyrm/templates/settings/federated_server.html:12 -msgid "Details" -msgstr "Détails" +#: bookwyrm/templates/settings/edit_server.html:16 +#: bookwyrm/templates/settings/server_blocklist.html:16 +#, fuzzy +#| msgid "Import book" +msgid "Import block list" +msgstr "Importer le livre" -#: bookwyrm/templates/settings/federated_server.html:15 -#| msgid "Software" -msgid "Software:" -msgstr "Logiciel :" +#: bookwyrm/templates/settings/edit_server.html:30 +#, fuzzy +#| msgid "Instance Name:" +msgid "Instance:" +msgstr "Nom de l’instance :" -#: bookwyrm/templates/settings/federated_server.html:19 -#| msgid "Description:" -msgid "Version:" -msgstr "Description :" - -#: bookwyrm/templates/settings/federated_server.html:23 -#| msgid "Status" +#: bookwyrm/templates/settings/edit_server.html:37 +#: bookwyrm/templates/settings/federated_server.html:29 +#: bookwyrm/templates/user_admin/user_info.html:34 msgid "Status:" msgstr "Statut :" -#: bookwyrm/templates/settings/federated_server.html:30 +#: bookwyrm/templates/settings/edit_server.html:41 +#: bookwyrm/templates/settings/federated_server.html:9 +#, fuzzy +#| msgid "Block" +msgid "Blocked" +msgstr "Bloquer" + +#: bookwyrm/templates/settings/edit_server.html:48 +#: bookwyrm/templates/settings/federated_server.html:21 +#: bookwyrm/templates/user_admin/user_info.html:26 +msgid "Software:" +msgstr "Logiciel :" + +#: bookwyrm/templates/settings/edit_server.html:55 +#: bookwyrm/templates/settings/federated_server.html:25 +#: bookwyrm/templates/user_admin/user_info.html:30 +msgid "Version:" +msgstr "Description :" + +#: bookwyrm/templates/settings/edit_server.html:64 +msgid "Notes:" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:18 +msgid "Details" +msgstr "Détails" + +#: bookwyrm/templates/settings/federated_server.html:36 #: bookwyrm/templates/user/user_layout.html:50 msgid "Activity" msgstr "Activité" -#: bookwyrm/templates/settings/federated_server.html:33 -#| msgid "Username:" +#: bookwyrm/templates/settings/federated_server.html:39 msgid "Users:" msgstr "Comptes :" -#: bookwyrm/templates/settings/federated_server.html:36 -#: bookwyrm/templates/settings/federated_server.html:43 +#: bookwyrm/templates/settings/federated_server.html:42 +#: bookwyrm/templates/settings/federated_server.html:49 msgid "View all" msgstr "Voir tous" -#: bookwyrm/templates/settings/federated_server.html:40 -#| msgid "Recent Imports" +#: bookwyrm/templates/settings/federated_server.html:46 msgid "Reports:" msgstr "Signalements :" -#: bookwyrm/templates/settings/federated_server.html:47 -#| msgid "followed you" +#: bookwyrm/templates/settings/federated_server.html:53 msgid "Followed by us:" msgstr "Suivi par nous :" -#: bookwyrm/templates/settings/federated_server.html:53 -#| msgid "followed you" +#: bookwyrm/templates/settings/federated_server.html:59 msgid "Followed by them:" msgstr "Suivi par eux :" -#: bookwyrm/templates/settings/federated_server.html:59 -#| msgid "Blocked Users" +#: bookwyrm/templates/settings/federated_server.html:65 msgid "Blocked by us:" msgstr "Bloqués par nous :" -#: bookwyrm/templates/settings/federation.html:13 +#: bookwyrm/templates/settings/federated_server.html:77 +#: bookwyrm/templates/user_admin/user_info.html:39 +msgid "Notes" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:80 +#, fuzzy +#| msgid "Edit Book" +msgid "Edit" +msgstr "Modifier le livre" + +#: bookwyrm/templates/settings/federated_server.html:100 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:3 +msgid "Actions" +msgstr "Actions" + +#: bookwyrm/templates/settings/federated_server.html:104 +#: bookwyrm/templates/snippets/block_button.html:5 +msgid "Block" +msgstr "Bloquer" + +#: bookwyrm/templates/settings/federated_server.html:105 +msgid "All users from this instance will be deactivated." +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:110 +#: bookwyrm/templates/snippets/block_button.html:10 +msgid "Un-block" +msgstr "Débloquer" + +#: bookwyrm/templates/settings/federated_server.html:111 +msgid "All users from this instance will be re-activated." +msgstr "" + +#: bookwyrm/templates/settings/federation.html:20 +#: bookwyrm/templates/user_admin/server_filter.html:5 msgid "Server name" msgstr "Nom du serveur" -#: bookwyrm/templates/settings/federation.html:17 -#| msgid "Federated" +#: bookwyrm/templates/settings/federation.html:24 msgid "Date federated" msgstr "Date de fédération" -#: bookwyrm/templates/settings/federation.html:21 +#: bookwyrm/templates/settings/federation.html:28 msgid "Software" msgstr "Logiciel" -#: bookwyrm/templates/settings/federation.html:24 +#: bookwyrm/templates/settings/federation.html:31 #: bookwyrm/templates/settings/manage_invite_requests.html:44 #: bookwyrm/templates/settings/status_filter.html:5 -#: bookwyrm/templates/settings/user_admin.html:32 +#: bookwyrm/templates/user_admin/user_admin.html:34 msgid "Status" msgstr "Statut" @@ -1847,7 +2053,6 @@ msgstr "Statut" #: bookwyrm/templates/settings/manage_invite_requests.html:11 #: bookwyrm/templates/settings/manage_invite_requests.html:25 #: bookwyrm/templates/settings/manage_invites.html:11 -#| msgid "Invites" msgid "Invite Requests" msgstr "Invitations" @@ -1856,12 +2061,10 @@ msgid "Ignored Invite Requests" msgstr "Invitations ignorées" #: bookwyrm/templates/settings/manage_invite_requests.html:35 -#| msgid "Federated" msgid "Date requested" msgstr "Date d’envoi" #: bookwyrm/templates/settings/manage_invite_requests.html:39 -#| msgid "Accept" msgid "Date accepted" msgstr "Date de validation" @@ -1870,18 +2073,15 @@ msgid "Email" msgstr "Email" #: bookwyrm/templates/settings/manage_invite_requests.html:47 -#| msgid "Notifications" msgid "Action" msgstr "Action" #: bookwyrm/templates/settings/manage_invite_requests.html:50 -#| msgid "Follow Requests" msgid "No requests" msgstr "Aucune demande" #: bookwyrm/templates/settings/manage_invite_requests.html:59 #: bookwyrm/templates/settings/status_filter.html:16 -#| msgid "Accept" msgid "Accepted" msgstr "Accepté(e)s" @@ -1955,6 +2155,27 @@ msgstr "Nombre de fois utilisée" msgid "No active invites" msgstr "Aucune invitation active" +#: bookwyrm/templates/settings/server_blocklist.html:6 +#, fuzzy +#| msgid "Import Books" +msgid "Import Blocklist" +msgstr "Importer des livres" + +#: bookwyrm/templates/settings/server_blocklist.html:26 +#: bookwyrm/templates/snippets/goal_progress.html:5 +msgid "Success!" +msgstr "Bravo !" + +#: bookwyrm/templates/settings/server_blocklist.html:30 +#, fuzzy +#| msgid "Successfully imported" +msgid "Successfully blocked:" +msgstr "Importation réussie" + +#: bookwyrm/templates/settings/server_blocklist.html:32 +msgid "Failed:" +msgstr "" + #: bookwyrm/templates/settings/site.html:15 msgid "Instance Name:" msgstr "Nom de l’instance :" @@ -2004,7 +2225,6 @@ msgid "Allow registration:" msgstr "Autoriser l’enregistrement :" #: bookwyrm/templates/settings/site.html:83 -#| msgid "Follow Requests" msgid "Allow invite requests:" msgstr "Autoriser les demandes d’invitation :" @@ -2012,66 +2232,28 @@ msgstr "Autoriser les demandes d’invitation :" msgid "Registration closed text:" msgstr "Texte affiché lorsque les enregistrements sont clos :" -#: bookwyrm/templates/settings/user_admin.html:7 -#, python-format -msgid "Users: %(server_name)s" -msgstr "Comptes : %(server_name)s" +#: bookwyrm/templates/snippets/book_cover.html:20 +#: bookwyrm/templates/snippets/search_result_text.html:10 +msgid "No cover" +msgstr "Aucune couverture" -#: bookwyrm/templates/settings/user_admin.html:20 -#| msgid "Username:" -msgid "Username" -msgstr "Nom du compte" - -#: bookwyrm/templates/settings/user_admin.html:24 -#| msgid "added" -msgid "Date Added" -msgstr "Date d’ajout" - -#: bookwyrm/templates/settings/user_admin.html:28 -msgid "Last Active" -msgstr "Dernière activité" - -#: bookwyrm/templates/settings/user_admin.html:36 -#| msgid "Remove" -msgid "Remote server" -msgstr "Serveur distant" - -#: bookwyrm/templates/settings/user_admin.html:45 -#| msgid "Activity" -msgid "Active" -msgstr "Actif" - -#: bookwyrm/templates/settings/user_admin.html:45 -msgid "Inactive" -msgstr "Inactif" - -#: bookwyrm/templates/settings/user_admin.html:50 -msgid "Not set" -msgstr "Non défini" - -#: bookwyrm/templates/snippets/block_button.html:5 -msgid "Block" -msgstr "Bloquer" - -#: bookwyrm/templates/snippets/block_button.html:10 -msgid "Un-block" -msgstr "Débloquer" - -#: bookwyrm/templates/snippets/book_titleby.html:3 +#: bookwyrm/templates/snippets/book_titleby.html:4 #, python-format msgid "%(title)s by " msgstr "%(title)s par " #: bookwyrm/templates/snippets/boost_button.html:8 #: bookwyrm/templates/snippets/boost_button.html:9 -#: bookwyrm/templates/snippets/status/status_body.html:52 -#: bookwyrm/templates/snippets/status/status_body.html:53 -msgid "Boost status" -msgstr "Partager le statut" +#, fuzzy +#| msgid "boosted" +msgid "Boost" +msgstr "partagé" +#: bookwyrm/templates/snippets/boost_button.html:15 #: bookwyrm/templates/snippets/boost_button.html:16 -#: bookwyrm/templates/snippets/boost_button.html:17 -msgid "Un-boost status" +#, fuzzy +#| msgid "Un-boost status" +msgid "Un-boost" msgstr "Annuler le partage du statut" #: bookwyrm/templates/snippets/content_warning_field.html:3 @@ -2090,60 +2272,60 @@ msgstr "Critique" msgid "Quote" msgstr "Citation" -#: bookwyrm/templates/snippets/create_status_form.html:18 +#: bookwyrm/templates/snippets/create_status_form.html:20 msgid "Comment:" msgstr "Commentaire :" -#: bookwyrm/templates/snippets/create_status_form.html:20 -#| msgid "Quote" +#: bookwyrm/templates/snippets/create_status_form.html:22 msgid "Quote:" msgstr "Citation :" -#: bookwyrm/templates/snippets/create_status_form.html:22 -#| msgid "Review" +#: bookwyrm/templates/snippets/create_status_form.html:24 msgid "Review:" msgstr "Critique :" -#: bookwyrm/templates/snippets/create_status_form.html:29 -#: bookwyrm/templates/user/shelf.html:81 -msgid "Rating" -msgstr "Note" +#: bookwyrm/templates/snippets/create_status_form.html:42 +#: bookwyrm/templates/snippets/status/layout.html:30 +#: bookwyrm/templates/snippets/status/layout.html:48 +#: bookwyrm/templates/snippets/status/layout.html:49 +msgid "Reply" +msgstr "Répondre" -#: bookwyrm/templates/snippets/create_status_form.html:56 +#: bookwyrm/templates/snippets/create_status_form.html:67 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:16 msgid "Progress:" msgstr "Progression :" -#: bookwyrm/templates/snippets/create_status_form.html:63 +#: bookwyrm/templates/snippets/create_status_form.html:75 #: bookwyrm/templates/snippets/readthrough_form.html:22 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:30 msgid "pages" msgstr "pages" -#: bookwyrm/templates/snippets/create_status_form.html:64 +#: bookwyrm/templates/snippets/create_status_form.html:76 #: bookwyrm/templates/snippets/readthrough_form.html:23 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:31 msgid "percent" msgstr "pourcent" -#: bookwyrm/templates/snippets/create_status_form.html:69 +#: bookwyrm/templates/snippets/create_status_form.html:82 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:36 #, python-format msgid "of %(pages)s pages" msgstr "sur %(pages)s pages" -#: bookwyrm/templates/snippets/create_status_form.html:81 +#: bookwyrm/templates/snippets/create_status_form.html:97 msgid "Include spoiler alert" msgstr "Afficher une alerte spoiler" -#: bookwyrm/templates/snippets/create_status_form.html:88 +#: bookwyrm/templates/snippets/create_status_form.html:104 #: bookwyrm/templates/snippets/privacy-icons.html:15 #: bookwyrm/templates/snippets/privacy-icons.html:16 #: bookwyrm/templates/snippets/privacy_select.html:19 msgid "Private" msgstr "Privé" -#: bookwyrm/templates/snippets/create_status_form.html:99 +#: bookwyrm/templates/snippets/create_status_form.html:115 msgid "Post" msgstr "Publier" @@ -2153,28 +2335,30 @@ msgstr "Supprimer ces dates de lecture ?" #: bookwyrm/templates/snippets/delete_readthrough_modal.html:7 #, python-format -msgid "You are deleting this readthrough and its %(count)s associated progress updates." +msgid "" +"You are deleting this readthrough and its %(count)s associated progress " +"updates." msgstr "Vous avez supprimé ce résumé et ses %(count)s progressions associées." #: bookwyrm/templates/snippets/delete_readthrough_modal.html:15 #: bookwyrm/templates/snippets/follow_request_buttons.html:13 +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:391 msgid "Delete" msgstr "Supprimer" #: bookwyrm/templates/snippets/fav_button.html:7 -#: bookwyrm/templates/snippets/fav_button.html:8 -#: bookwyrm/templates/snippets/status/status_body.html:56 -#: bookwyrm/templates/snippets/status/status_body.html:57 -msgid "Like status" -msgstr "Ajouter le statut aux favoris" +#: bookwyrm/templates/snippets/fav_button.html:9 +msgid "Like" +msgstr "" #: bookwyrm/templates/snippets/fav_button.html:15 #: bookwyrm/templates/snippets/fav_button.html:16 -msgid "Un-like status" +#, fuzzy +#| msgid "Un-like status" +msgid "Un-like" msgstr "Retirer le statut des favoris" #: bookwyrm/templates/snippets/filters_panel/filters_panel.html:7 -#| msgid "Show less" msgid "Show filters" msgstr "Afficher les filtres" @@ -2187,7 +2371,6 @@ msgid "Apply filters" msgstr "Appliquer les filtres" #: bookwyrm/templates/snippets/filters_panel/filters_panel.html:26 -#| msgid "Clear search" msgid "Clear filters" msgstr "Annuler les filtres" @@ -2196,7 +2379,6 @@ msgid "Follow" msgstr "S’abonner" #: bookwyrm/templates/snippets/follow_button.html:18 -#| msgid "Send follow request" msgid "Undo follow request" msgstr "Annuler la demande d’abonnement" @@ -2213,14 +2395,6 @@ msgstr "Accepter" msgid "No rating" msgstr "Aucune note" -#: bookwyrm/templates/snippets/form_rate_stars.html:45 -#: bookwyrm/templates/snippets/stars.html:7 -#, python-format -msgid "%(rating)s star" -msgid_plural "%(rating)s stars" -msgstr[0] "%(rating)s étoile" -msgstr[1] "%(rating)s étoiles" - #: bookwyrm/templates/snippets/generated_status/goal.html:1 #, python-format msgid "set a goal to read %(counter)s book in %(year)s" @@ -2230,18 +2404,24 @@ msgstr[1] "souhaite lire %(counter)s livres en %(year)s" #: bookwyrm/templates/snippets/generated_status/rating.html:3 #, python-format -#| msgid "%(title)s by " -msgid "Rated %(title)s: %(display_rating)s star" -msgid_plural "Rated %(title)s: %(display_rating)s stars" +msgid "" +"Rated %(title)s: %(display_rating)s star" +msgid_plural "" +"Rated %(title)s: %(display_rating)s stars" msgstr[0] "A noté %(title)s : %(display_rating)s star" -msgstr[1] "A noté %(title)s : %(display_rating)s stars" +msgstr[1] "" +"A noté %(title)s : %(display_rating)s stars" #: bookwyrm/templates/snippets/generated_status/review_pure_name.html:4 #, python-format -msgid "Review of \"%(book_title)s\" (%(display_rating)s star): %(review_title)s" -msgid_plural "Review of \"%(book_title)s\" (%(display_rating)s stars): %(review_title)s" -msgstr[0] "Critique de « %(book_title)s » (%(display_rating)s star): %(review_title)s" -msgstr[1] "Critique de « %(book_title)s » (%(display_rating)s stars) : %(review_title)s" +msgid "" +"Review of \"%(book_title)s\" (%(display_rating)s star): %(review_title)s" +msgid_plural "" +"Review of \"%(book_title)s\" (%(display_rating)s stars): %(review_title)s" +msgstr[0] "" +"Critique de « %(book_title)s » (%(display_rating)s star): %(review_title)s" +msgstr[1] "" +"Critique de « %(book_title)s » (%(display_rating)s stars) : %(review_title)s" #: bookwyrm/templates/snippets/generated_status/review_pure_name.html:8 #, python-format @@ -2250,8 +2430,12 @@ msgstr "Critique de « %(book_title)s » : %(review_title)s" #: bookwyrm/templates/snippets/goal_card.html:23 #, python-format -msgid "You can set or change your reading goal any time from your profile page" -msgstr "Vous pouvez définir ou changer votre défi lecture à n’importe quel moment depuis votre profil" +msgid "" +"You can set or change your reading goal any time from your profile page" +msgstr "" +"Vous pouvez définir ou changer votre défi lecture à n’importe quel moment " +"depuis votre profil" #: bookwyrm/templates/snippets/goal_form.html:9 msgid "Reading goal:" @@ -2276,10 +2460,6 @@ msgstr "Publier sur le fil d’actualité" msgid "Set goal" msgstr "Valider ce défi" -#: bookwyrm/templates/snippets/goal_progress.html:5 -msgid "Success!" -msgstr "Bravo !" - #: bookwyrm/templates/snippets/goal_progress.html:7 #, python-format msgid "%(percent)s%% complete!" @@ -2287,23 +2467,28 @@ msgstr "%(percent)s%% terminé !" #: bookwyrm/templates/snippets/goal_progress.html:10 #, python-format -msgid "You've read %(read_count)s of %(goal_count)s books." -msgstr "Vous avez lu %(read_count)s sur %(goal_count)s livres." +msgid "" +"You've read %(read_count)s of %(goal_count)s books." +msgstr "" +"Vous avez lu %(read_count)s sur %(goal_count)s livres." #: bookwyrm/templates/snippets/goal_progress.html:12 #, python-format -msgid "%(username)s has read %(read_count)s of %(goal_count)s books." -msgstr "%(username)s a lu %(read_count)s sur %(goal_count)s livres." +msgid "" +"%(username)s has read %(read_count)s of %(goal_count)s " +"books." +msgstr "" +"%(username)s a lu %(read_count)s sur %(goal_count)s " +"livres." #: bookwyrm/templates/snippets/page_text.html:4 #, python-format -#| msgid "of %(pages)s pages" msgid "page %(page)s of %(total_pages)s" msgstr "page %(page)s sur %(total_pages)s pages" #: bookwyrm/templates/snippets/page_text.html:6 #, python-format -#| msgid "of %(book.pages)s pages" msgid "page %(page)s" msgstr "page %(page)s" @@ -2374,7 +2559,6 @@ msgid "Edit read dates" msgstr "Modifier les date de lecture" #: bookwyrm/templates/snippets/readthrough.html:61 -#| msgid "Delete these read dates?" msgid "Delete these read dates" msgstr "Supprimer ces dates de lecture" @@ -2398,35 +2582,29 @@ msgid "Sign Up" msgstr "S’enregistrer" #: bookwyrm/templates/snippets/report_button.html:5 -#| msgid "Import" msgid "Report" msgstr "Signaler" #: bookwyrm/templates/snippets/rss_title.html:5 -#: bookwyrm/templates/snippets/status/status_header.html:11 +#: bookwyrm/templates/snippets/status/status_header.html:34 msgid "rated" msgstr "a noté" #: bookwyrm/templates/snippets/rss_title.html:7 -#: bookwyrm/templates/snippets/status/status_header.html:13 +#: bookwyrm/templates/snippets/status/status_header.html:36 msgid "reviewed" msgstr "a écrit une critique de" #: bookwyrm/templates/snippets/rss_title.html:9 -#: bookwyrm/templates/snippets/status/status_header.html:15 +#: bookwyrm/templates/snippets/status/status_header.html:38 msgid "commented on" msgstr "a commenté" #: bookwyrm/templates/snippets/rss_title.html:11 -#: bookwyrm/templates/snippets/status/status_header.html:17 +#: bookwyrm/templates/snippets/status/status_header.html:40 msgid "quoted" msgstr "a cité" -#: bookwyrm/templates/snippets/search_result_text.html:10 -#| msgid "Add cover" -msgid "No cover" -msgstr "Aucune couverture" - #: bookwyrm/templates/snippets/search_result_text.html:22 #, python-format msgid "by %(author)s" @@ -2437,7 +2615,6 @@ msgid "Import book" msgstr "Importer le livre" #: bookwyrm/templates/snippets/shelf_selector.html:4 -#| msgid "Your books" msgid "Move book" msgstr "Déplacer le livre" @@ -2447,8 +2624,7 @@ msgid "Finish \"%(book_title)s\"" msgstr "Terminer « %(book_title)s »" #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:5 -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:35 -#| msgid "Updated:" +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:34 msgid "Update progress" msgstr "Progression de la mise à jour" @@ -2457,12 +2633,10 @@ msgid "More shelves" msgstr "Plus d’étagères" #: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:8 -#| msgid "Started reading" msgid "Start reading" msgstr "Commencer la lecture" #: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:13 -#| msgid "Finished reading" msgid "Finish reading" msgstr "Terminer la lecture" @@ -2471,9 +2645,8 @@ msgstr "Terminer la lecture" msgid "Want to read" msgstr "Je veux le lire" -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:48 +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:45 #, python-format -#| msgid "Join %(name)s" msgid "Remove from %(name)s" msgstr "Retirer de %(name)s" @@ -2487,99 +2660,77 @@ msgstr "Commencer « %(book_title)s »" msgid "Want to Read \"%(book_title)s\"" msgstr "A envie de lire « %(book_title)s »" +#: bookwyrm/templates/snippets/status/content_status.html:67 +#: bookwyrm/templates/snippets/trimmed_text.html:14 +msgid "Show more" +msgstr "Déplier" + +#: bookwyrm/templates/snippets/status/content_status.html:82 +#: bookwyrm/templates/snippets/trimmed_text.html:29 +msgid "Show less" +msgstr "Replier" + +#: bookwyrm/templates/snippets/status/content_status.html:112 +msgid "Open image in new window" +msgstr "Ouvrir l’image dans une nouvelle fenêtre" + +#: bookwyrm/templates/snippets/status/layout.html:22 +#: bookwyrm/templates/snippets/status/status_options.html:17 +msgid "Delete status" +msgstr "Supprimer le statut" + +#: bookwyrm/templates/snippets/status/layout.html:52 +#: bookwyrm/templates/snippets/status/layout.html:53 +msgid "Boost status" +msgstr "Partager le statut" + +#: bookwyrm/templates/snippets/status/layout.html:56 +#: bookwyrm/templates/snippets/status/layout.html:57 +msgid "Like status" +msgstr "Ajouter le statut aux favoris" + #: bookwyrm/templates/snippets/status/status.html:9 msgid "boosted" msgstr "partagé" -#: bookwyrm/templates/snippets/status/status_body.html:27 -#: bookwyrm/templates/snippets/status/status_options.html:18 -msgid "Delete status" -msgstr "Supprimer le statut" - -#: bookwyrm/templates/snippets/status/status_body.html:35 -#: bookwyrm/templates/snippets/status/status_body.html:48 -#: bookwyrm/templates/snippets/status/status_body.html:49 -msgid "Reply" -msgstr "Répondre" - -#: bookwyrm/templates/snippets/status/status_content.html:18 -#: bookwyrm/templates/snippets/trimmed_text.html:15 -msgid "Show more" -msgstr "Déplier" - -#: bookwyrm/templates/snippets/status/status_content.html:25 -#: bookwyrm/templates/snippets/trimmed_text.html:25 -msgid "Show less" -msgstr "Replier" - -#: bookwyrm/templates/snippets/status/status_content.html:46 -msgid "Open image in new window" -msgstr "Ouvrir l’image dans une nouvelle fenêtre" - -#: bookwyrm/templates/snippets/status/status_header.html:22 +#: bookwyrm/templates/snippets/status/status_header.html:44 #, python-format -#| msgid "favorited your %(preview_name)s" -msgid "replied to %(username)s's review" -msgstr "a répondu à la critique de %(username)s" - -#: bookwyrm/templates/snippets/status/status_header.html:24 -#, python-format -#| msgid "replied to your status" -msgid "replied to %(username)s's comment" -msgstr "a répondu au commentaire de %(username)s" - -#: bookwyrm/templates/snippets/status/status_header.html:26 -#, python-format -#| msgid "replied to your status" -msgid "replied to %(username)s's quote" -msgstr "a répondu à la citation de %(username)s" - -#: bookwyrm/templates/snippets/status/status_header.html:28 -#, python-format -#| msgid "replied to your status" -msgid "replied to %(username)s's status" -msgstr "a répondu au statut de %(username)s" +msgid "" +"replied to %(username)s's status" +msgstr "" +"a répondu au statut de %(username)s" #: bookwyrm/templates/snippets/status/status_options.html:7 #: bookwyrm/templates/snippets/user_options.html:7 msgid "More options" msgstr "Plus d’options" -#: bookwyrm/templates/snippets/status/status_options.html:27 -#| msgid "Delete these read dates?" +#: bookwyrm/templates/snippets/status/status_options.html:26 msgid "Delete & re-draft" msgstr "Supprimer & recommencer la rédaction" +#: bookwyrm/templates/snippets/status/status_options.html:35 +#: bookwyrm/templates/snippets/user_options.html:13 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:6 +msgid "Send direct message" +msgstr "Envoyer un message direct" + #: bookwyrm/templates/snippets/switch_edition_button.html:5 msgid "Switch to this edition" msgstr "Changer vers cette édition" #: bookwyrm/templates/snippets/table-sort-header.html:6 -#| msgid "Started reading" msgid "Sorted ascending" msgstr "Trié par ordre croissant" #: bookwyrm/templates/snippets/table-sort-header.html:10 -#| msgid "Started reading" msgid "Sorted descending" msgstr "Trié par ordre décroissant" -#: bookwyrm/templates/snippets/tag.html:14 -msgid "Remove tag" -msgstr "Supprimer le tag" - -#: bookwyrm/templates/snippets/tag.html:18 -msgid "Add tag" -msgstr "Ajouter un tag" - -#: bookwyrm/templates/tag.html:9 -#, python-format -msgid "Books tagged \"%(tag.name)s\"" -msgstr "Livres tagués « %(tag.name)s »" - #: bookwyrm/templates/user/books_header.html:5 #, python-format -#| msgid "%(username)s has no followers" msgid "%(username)s's books" msgstr "Livres de %(username)s" @@ -2616,13 +2767,8 @@ msgstr "Abonné(e) à" msgid "%(username)s isn't following any users" msgstr "%(username)s n’est abonné(e) à personne" -#: bookwyrm/templates/user/lists.html:9 -msgid "Your Lists" -msgstr "Vos listes" - #: bookwyrm/templates/user/lists.html:11 #, python-format -#| msgid "Join %(name)s" msgid "Lists: %(username)s" msgstr "Listes : %(username)s" @@ -2630,8 +2776,7 @@ msgstr "Listes : %(username)s" msgid "Create list" msgstr "Créer une liste" -#: bookwyrm/templates/user/shelf.html:24 bookwyrm/views/shelf.py:56 -#| msgid "books" +#: bookwyrm/templates/user/shelf.html:24 bookwyrm/views/shelf.py:51 msgid "All books" msgstr "Tous les livres" @@ -2643,23 +2788,23 @@ msgstr "Créer l’étagère" msgid "Edit shelf" msgstr "Modifier l’étagère" -#: bookwyrm/templates/user/shelf.html:78 +#: bookwyrm/templates/user/shelf.html:77 bookwyrm/templates/user/shelf.html:99 msgid "Shelved" msgstr "Ajouté à une étagère" -#: bookwyrm/templates/user/shelf.html:79 +#: bookwyrm/templates/user/shelf.html:78 bookwyrm/templates/user/shelf.html:103 msgid "Started" msgstr "Commencé" -#: bookwyrm/templates/user/shelf.html:80 +#: bookwyrm/templates/user/shelf.html:79 bookwyrm/templates/user/shelf.html:106 msgid "Finished" msgstr "Terminé" -#: bookwyrm/templates/user/shelf.html:129 +#: bookwyrm/templates/user/shelf.html:132 msgid "This shelf is empty." msgstr "Cette étagère est vide" -#: bookwyrm/templates/user/shelf.html:135 +#: bookwyrm/templates/user/shelf.html:138 msgid "Delete shelf" msgstr "Supprimer l’étagère" @@ -2669,7 +2814,6 @@ msgstr "Modifier le profil" #: bookwyrm/templates/user/user.html:34 #, python-format -#| msgid "See all %(size)s" msgid "View all %(size)s" msgstr "Voir les %(size)s" @@ -2703,7 +2847,6 @@ msgid "Reading Goal" msgstr "Défi lecture" #: bookwyrm/templates/user/user_layout.html:68 -#| msgid "Book" msgid "Books" msgstr "Livres" @@ -2714,7 +2857,6 @@ msgstr "Enregistré(e) %(date)s" #: bookwyrm/templates/user/user_preview.html:15 #, python-format -#| msgid "%(username)s has no followers" msgid "%(counter)s follower" msgid_plural "%(counter)s followers" msgstr[0] "%(counter)s abonnement" @@ -2725,6 +2867,79 @@ msgstr[1] "%(counter)s abonnements" msgid "%(counter)s following" msgstr "%(counter)s abonnements" +#: bookwyrm/templates/user_admin/user.html:11 +#, fuzzy +#| msgid "Back to reports" +msgid "Back to users" +msgstr "Retour aux signalements" + +#: bookwyrm/templates/user_admin/user_admin.html:7 +#, python-format +msgid "Users: %(server_name)s" +msgstr "Comptes : %(server_name)s" + +#: bookwyrm/templates/user_admin/user_admin.html:22 +#: bookwyrm/templates/user_admin/username_filter.html:5 +msgid "Username" +msgstr "Nom du compte" + +#: bookwyrm/templates/user_admin/user_admin.html:26 +msgid "Date Added" +msgstr "Date d’ajout" + +#: bookwyrm/templates/user_admin/user_admin.html:30 +msgid "Last Active" +msgstr "Dernière activité" + +#: bookwyrm/templates/user_admin/user_admin.html:38 +msgid "Remote server" +msgstr "Serveur distant" + +#: bookwyrm/templates/user_admin/user_admin.html:47 +msgid "Active" +msgstr "Actif" + +#: bookwyrm/templates/user_admin/user_admin.html:47 +msgid "Inactive" +msgstr "Inactif" + +#: bookwyrm/templates/user_admin/user_admin.html:52 +#: bookwyrm/templates/user_admin/user_info.html:49 +msgid "Not set" +msgstr "Non défini" + +#: bookwyrm/templates/user_admin/user_info.html:5 +#, fuzzy +#| msgid "Details" +msgid "User details" +msgstr "Détails" + +#: bookwyrm/templates/user_admin/user_info.html:14 +msgid "View user profile" +msgstr "Voir le profil" + +#: bookwyrm/templates/user_admin/user_info.html:20 +#, fuzzy +#| msgid "Instance Settings" +msgid "Instance details" +msgstr "Paramètres de l’instance" + +#: bookwyrm/templates/user_admin/user_info.html:46 +msgid "View instance" +msgstr "" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:11 +msgid "Suspend user" +msgstr "" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:13 +msgid "Un-suspend user" +msgstr "" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:21 +msgid "Access level:" +msgstr "" + #: bookwyrm/views/password.py:32 msgid "No user with that email address was found." msgstr "Aucun compte avec cette adresse email n’a été trouvé." @@ -2734,6 +2949,1444 @@ msgstr "Aucun compte avec cette adresse email n’a été trouvé." msgid "A password reset link sent to %s" msgstr "Un lien de réinitialisation a été envoyé à %s." +#: venv3/lib/python3.8/site-packages/_pytest/config/argparsing.py:442 +#, python-format +msgid "ambiguous option: %(option)s could match %(matches)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/contrib/messages/apps.py:7 +#, fuzzy +#| msgid "All messages" +msgid "Messages" +msgstr "Tous les messages" + +#: venv3/lib/python3.8/site-packages/django/contrib/sitemaps/apps.py:7 +msgid "Site Maps" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/contrib/staticfiles/apps.py:9 +msgid "Static Files" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/contrib/syndication/apps.py:7 +msgid "Syndication" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/paginator.py:45 +msgid "That page number is not an integer" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/paginator.py:47 +msgid "That page number is less than 1" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/paginator.py:52 +msgid "That page contains no results" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:32 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid value." +msgstr "Adresse email :" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:103 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:659 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid URL." +msgstr "Adresse email :" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:155 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid integer." +msgstr "Adresse email :" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:166 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid email address." +msgstr "Adresse email :" + +#. Translators: "letters" means latin letters: a-z and A-Z. +#: venv3/lib/python3.8/site-packages/django/core/validators.py:240 +msgid "" +"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:247 +msgid "" +"Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " +"hyphens." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:256 +#: venv3/lib/python3.8/site-packages/django/core/validators.py:276 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid IPv4 address." +msgstr "Adresse email :" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:261 +#: venv3/lib/python3.8/site-packages/django/core/validators.py:277 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid IPv6 address." +msgstr "Adresse email :" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:271 +#: venv3/lib/python3.8/site-packages/django/core/validators.py:275 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid IPv4 or IPv6 address." +msgstr "Adresse email :" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:305 +msgid "Enter only digits separated by commas." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:311 +#, python-format +msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:343 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:352 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:362 +#, python-format +msgid "" +"Ensure this value has at least %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at least %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:377 +#, python-format +msgid "" +"Ensure this value has at most %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at most %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:396 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:291 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:326 +#, fuzzy +#| msgid "Series number:" +msgid "Enter a number." +msgstr "Numéro dans la série :" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:398 +#, python-format +msgid "Ensure that there are no more than %(max)s digit in total." +msgid_plural "Ensure that there are no more than %(max)s digits in total." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:403 +#, python-format +msgid "Ensure that there are no more than %(max)s decimal place." +msgid_plural "Ensure that there are no more than %(max)s decimal places." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:408 +#, python-format +msgid "" +"Ensure that there are no more than %(max)s digit before the decimal point." +msgid_plural "" +"Ensure that there are no more than %(max)s digits before the decimal point." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:470 +#, python-format +msgid "" +"File extension “%(extension)s” is not allowed. Allowed extensions are: " +"%(allowed_extensions)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:522 +msgid "Null characters are not allowed." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/base.py:1181 +#: venv3/lib/python3.8/site-packages/django/forms/models.py:759 +msgid "and" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/base.py:1183 +#, fuzzy, python-format +#| msgid "A user with this email already exists." +msgid "%(model_name)s with this %(field_labels)s already exists." +msgstr "Cet email est déjà associé à un compte." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:104 +#, fuzzy, python-format +#| msgid "%(value)s is not a valid remote_id" +msgid "Value %(value)r is not a valid choice." +msgstr "%(value)s n’est pas une remote_id valide." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:105 +#, fuzzy +#| msgid "This shelf is empty." +msgid "This field cannot be null." +msgstr "Cette étagère est vide" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:106 +msgid "This field cannot be blank." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:107 +#, fuzzy, python-format +#| msgid "A user with this email already exists." +msgid "%(model_name)s with this %(field_label)s already exists." +msgstr "Cet email est déjà associé à un compte." + +#. Translators: The 'lookup_type' is one of 'date', 'year' or 'month'. +#. Eg: "Title must be unique for pub_date year" +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:111 +#, python-format +msgid "" +"%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:130 +#, python-format +msgid "Field of type: %(field_type)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:937 +#, python-format +msgid "“%(value)s” value must be either True or False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:938 +#, python-format +msgid "“%(value)s” value must be either True, False, or None." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:940 +msgid "Boolean (Either True or False)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:981 +#, python-format +msgid "String (up to %(max_length)s)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1045 +#, fuzzy +#| msgid "No active invites" +msgid "Comma-separated integers" +msgstr "Aucune invitation active" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1094 +#, python-format +msgid "" +"“%(value)s” value has an invalid date format. It must be in YYYY-MM-DD " +"format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1096 +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1239 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD) but it is an invalid " +"date." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1099 +msgid "Date (without time)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1237 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." +"uuuuuu]][TZ] format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1241 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" +"[TZ]) but it is an invalid date/time." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1245 +msgid "Date (with time)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1393 +#, python-format +msgid "“%(value)s” value must be a decimal number." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1395 +#, fuzzy +#| msgid "Series number:" +msgid "Decimal number" +msgstr "Numéro dans la série :" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1534 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in [DD] [[HH:]MM:]ss[." +"uuuuuu] format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1537 +#, fuzzy +#| msgid "List curation:" +msgid "Duration" +msgstr "Modération de la liste :" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1587 +#, fuzzy +#| msgid "Email address:" +msgid "Email address" +msgstr "Adresse email :" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1610 +msgid "File path" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1676 +#, python-format +msgid "“%(value)s” value must be a float." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1678 +msgid "Floating point number" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1716 +#, python-format +msgid "“%(value)s” value must be an integer." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1718 +msgid "Integer" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1801 +#, fuzzy +#| msgid "No active invites" +msgid "Big (8 byte) integer" +msgstr "Aucune invitation active" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1817 +#, fuzzy +#| msgid "Email address:" +msgid "IPv4 address" +msgstr "Adresse email :" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1848 +#, fuzzy +#| msgid "Email address:" +msgid "IP address" +msgstr "Adresse email :" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1928 +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1929 +#, python-format +msgid "“%(value)s” value must be either None, True or False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1931 +msgid "Boolean (Either True, False or None)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1966 +#, fuzzy +#| msgid "No active invites" +msgid "Positive integer" +msgstr "Aucune invitation active" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1979 +#, fuzzy +#| msgid "No active invites" +msgid "Positive small integer" +msgstr "Aucune invitation active" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1993 +#, python-format +msgid "Slug (up to %(max_length)s)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2025 +msgid "Small integer" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2032 +msgid "Text" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2060 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] " +"format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2062 +#, python-format +msgid "" +"“%(value)s” value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an " +"invalid time." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2065 +msgid "Time" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2191 +msgid "URL" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2213 +msgid "Raw binary data" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2278 +#, fuzzy, python-format +#| msgid "%(value)s is not a valid username" +msgid "“%(value)s” is not a valid UUID." +msgstr "%(value)s n’est pas un nom de compte valide." + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2280 +msgid "Universally unique identifier" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/files.py:221 +msgid "File" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/files.py:360 +#, fuzzy +#| msgid "Images" +msgid "Image" +msgstr "Images" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:778 +#, python-format +msgid "%(model)s instance with %(field)s %(value)r does not exist." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:780 +msgid "Foreign Key (type determined by related field)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1012 +#, fuzzy +#| msgid "Relationships" +msgid "One-to-one relationship" +msgstr "Relations" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1066 +#, fuzzy, python-format +#| msgid "Relationships" +msgid "%(from)s-%(to)s relationship" +msgstr "Relations" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1067 +#, fuzzy, python-format +#| msgid "Relationships" +msgid "%(from)s-%(to)s relationships" +msgstr "Relations" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1109 +#, fuzzy +#| msgid "Relationships" +msgid "Many-to-many relationship" +msgstr "Relations" + +#. Translators: If found as last label character, these punctuation +#. characters will prevent the default label_suffix to be appended to the label +#: venv3/lib/python3.8/site-packages/django/forms/boundfield.py:149 +msgid ":?.!" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:53 +#, fuzzy +#| msgid "This shelf is empty." +msgid "This field is required." +msgstr "Cette étagère est vide" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:246 +#, fuzzy +#| msgid "Series number:" +msgid "Enter a whole number." +msgstr "Numéro dans la série :" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:397 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:1127 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid date." +msgstr "Adresse email :" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:421 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:1128 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid time." +msgstr "Adresse email :" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:443 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid date/time." +msgstr "Adresse email :" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:472 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid duration." +msgstr "Adresse email :" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:473 +#, python-brace-format +msgid "The number of days must be between {min_days} and {max_days}." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:533 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:534 +msgid "No file was submitted." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:535 +#, fuzzy +#| msgid "This shelf is empty." +msgid "The submitted file is empty." +msgstr "Cette étagère est vide" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:537 +#, python-format +msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." +msgid_plural "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:540 +msgid "Please either submit a file or check the clear checkbox, not both." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:601 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:763 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:853 +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1275 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:854 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:969 +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1274 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a list of values." +msgstr "Adresse email :" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:970 +msgid "Enter a complete value." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:1186 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid UUID." +msgstr "Adresse email :" + +#. Translators: This is the default suffix added to form field labels +#: venv3/lib/python3.8/site-packages/django/forms/forms.py:81 +msgid ":" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/forms.py:207 +#, python-format +msgid "(Hidden field %(name)s) %(error)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:93 +msgid "ManagementForm data is missing or has been tampered with" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:345 +#, python-format +msgid "Please submit %d or fewer forms." +msgid_plural "Please submit %d or fewer forms." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:352 +#, python-format +msgid "Please submit %d or more forms." +msgid_plural "Please submit %d or more forms." +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:379 +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:386 +#, fuzzy +#| msgid "Order by" +msgid "Order" +msgstr "Trier par" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:754 +#, python-format +msgid "Please correct the duplicate data for %(field)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:758 +#, python-format +msgid "Please correct the duplicate data for %(field)s, which must be unique." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:764 +#, python-format +msgid "" +"Please correct the duplicate data for %(field_name)s which must be unique " +"for the %(lookup)s in %(date_field)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:773 +msgid "Please correct the duplicate values below." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1094 +msgid "The inline value did not match the parent instance." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1161 +msgid "Select a valid choice. That choice is not one of the available choices." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1277 +#, fuzzy, python-format +#| msgid "%(value)s is not a valid username" +msgid "“%(pk)s” is not a valid value." +msgstr "%(value)s n’est pas un nom de compte valide." + +#: venv3/lib/python3.8/site-packages/django/forms/utils.py:162 +#, python-format +msgid "" +"%(datetime)s couldn’t be interpreted in time zone %(current_timezone)s; it " +"may be ambiguous or it may not exist." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:395 +msgid "Clear" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:396 +#, fuzzy +#| msgid "Started reading" +msgid "Currently" +msgstr "Commencer la lecture" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:397 +#, fuzzy +#| msgid "Change shelf" +msgid "Change" +msgstr "Changer d’étagère" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:709 +msgid "Unknown" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:710 +msgid "Yes" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:711 +msgid "No" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:788 +msgid "yes,no,maybe" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:817 +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:834 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:836 +#, python-format +msgid "%s KB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:838 +#, python-format +msgid "%s MB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:840 +#, python-format +msgid "%s GB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:842 +#, python-format +msgid "%s TB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:844 +#, python-format +msgid "%s PB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:62 +msgid "p.m." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:63 +msgid "a.m." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:68 +msgid "PM" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:69 +msgid "AM" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:150 +msgid "midnight" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:152 +msgid "noon" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:295 +msgid "Monday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:296 +msgid "Tuesday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:297 +msgid "Wednesday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:298 +msgid "Thursday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:299 +msgid "Friday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:7 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:300 +msgid "Saturday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:7 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:301 +msgid "Sunday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Mon" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Tue" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Wed" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Thu" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Fri" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:11 +#, fuzzy +#| msgid "Status" +msgid "Sat" +msgstr "Statut" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:11 +msgid "Sun" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:281 +msgid "January" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:282 +msgid "February" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:283 +#, fuzzy +#| msgid "Search" +msgid "March" +msgstr "Chercher" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:284 +msgid "April" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:285 +msgid "May" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:286 +msgid "June" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:287 +msgid "July" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:288 +msgid "August" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:289 +#, fuzzy +#| msgid "Series number:" +msgid "September" +msgstr "Numéro dans la série :" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:290 +msgid "October" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:291 +msgid "November" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:16 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:292 +#, fuzzy +#| msgid "Series number:" +msgid "December" +msgstr "Numéro dans la série :" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "jan" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "feb" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "mar" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "apr" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "may" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "jun" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "jul" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "aug" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "sep" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "oct" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "nov" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "dec" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:23 +msgctxt "abbrev. month" +msgid "Jan." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:24 +msgctxt "abbrev. month" +msgid "Feb." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:25 +#, fuzzy +#| msgid "Search" +msgctxt "abbrev. month" +msgid "March" +msgstr "Chercher" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:26 +msgctxt "abbrev. month" +msgid "April" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:27 +msgctxt "abbrev. month" +msgid "May" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:28 +msgctxt "abbrev. month" +msgid "June" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:29 +msgctxt "abbrev. month" +msgid "July" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:30 +msgctxt "abbrev. month" +msgid "Aug." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:31 +msgctxt "abbrev. month" +msgid "Sept." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:32 +msgctxt "abbrev. month" +msgid "Oct." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:33 +msgctxt "abbrev. month" +msgid "Nov." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:34 +msgctxt "abbrev. month" +msgid "Dec." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:37 +msgctxt "alt. month" +msgid "January" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:38 +msgctxt "alt. month" +msgid "February" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:39 +#, fuzzy +#| msgid "Search" +msgctxt "alt. month" +msgid "March" +msgstr "Chercher" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:40 +msgctxt "alt. month" +msgid "April" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:41 +msgctxt "alt. month" +msgid "May" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:42 +msgctxt "alt. month" +msgid "June" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:43 +msgctxt "alt. month" +msgid "July" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:44 +msgctxt "alt. month" +msgid "August" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:45 +#, fuzzy +#| msgid "Series number:" +msgctxt "alt. month" +msgid "September" +msgstr "Numéro dans la série :" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:46 +msgctxt "alt. month" +msgid "October" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:47 +msgctxt "alt. month" +msgid "November" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:48 +#, fuzzy +#| msgid "Series number:" +msgctxt "alt. month" +msgid "December" +msgstr "Numéro dans la série :" + +#: venv3/lib/python3.8/site-packages/django/utils/ipv6.py:8 +#, fuzzy +#| msgid "Email address:" +msgid "This is not a valid IPv6 address." +msgstr "Adresse email :" + +#: venv3/lib/python3.8/site-packages/django/utils/text.py:69 +#, python-format +msgctxt "String to return when truncating text" +msgid "%(truncated_text)s…" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/text.py:235 +msgid "or" +msgstr "" + +#. Translators: This string is used as a separator between list elements +#: venv3/lib/python3.8/site-packages/django/utils/text.py:254 +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:83 +msgid ", " +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:9 +#, python-format +msgid "%d year" +msgid_plural "%d years" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:10 +#, python-format +msgid "%d month" +msgid_plural "%d months" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:11 +#, python-format +msgid "%d week" +msgid_plural "%d weeks" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:12 +#, python-format +msgid "%d day" +msgid_plural "%d days" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:13 +#, python-format +msgid "%d hour" +msgid_plural "%d hours" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:14 +#, python-format +msgid "%d minute" +msgid_plural "%d minutes" +msgstr[0] "" +msgstr[1] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:72 +msgid "0 minutes" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:110 +msgid "Forbidden" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:111 +msgid "CSRF verification failed. Request aborted." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:115 +msgid "" +"You are seeing this message because this HTTPS site requires a “Referer " +"header” to be sent by your Web browser, but none was sent. This header is " +"required for security reasons, to ensure that your browser is not being " +"hijacked by third parties." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:120 +msgid "" +"If you have configured your browser to disable “Referer” headers, please re-" +"enable them, at least for this site, or for HTTPS connections, or for “same-" +"origin” requests." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:124 +msgid "" +"If you are using the tag or " +"including the “Referrer-Policy: no-referrer” header, please remove them. The " +"CSRF protection requires the “Referer” header to do strict referer checking. " +"If you’re concerned about privacy, use alternatives like for links to third-party sites." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:132 +msgid "" +"You are seeing this message because this site requires a CSRF cookie when " +"submitting forms. This cookie is required for security reasons, to ensure " +"that your browser is not being hijacked by third parties." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:137 +msgid "" +"If you have configured your browser to disable cookies, please re-enable " +"them, at least for this site, or for “same-origin” requests." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:142 +msgid "More information is available with DEBUG=True." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:41 +msgid "No year specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:61 +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:111 +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:208 +msgid "Date out of range" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:90 +msgid "No month specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:142 +msgid "No day specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:188 +msgid "No week specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:338 +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:367 +#, python-format +msgid "No %(verbose_name_plural)s available" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:589 +#, python-format +msgid "" +"Future %(verbose_name_plural)s not available because %(class_name)s." +"allow_future is False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:623 +#, python-format +msgid "Invalid date string “%(datestr)s” given format “%(format)s”" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/detail.py:54 +#, fuzzy, python-format +#| msgid "No books found matching the query \"%(query)s\"" +msgid "No %(verbose_name)s found matching the query" +msgstr "Aucun livre trouvé pour la requête « %(query)s »" + +#: venv3/lib/python3.8/site-packages/django/views/generic/list.py:67 +msgid "Page is not “last”, nor can it be converted to an int." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/list.py:72 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/list.py:154 +#, python-format +msgid "Empty list and “%(class_name)s.allow_empty” is False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/static.py:40 +msgid "Directory indexes are not allowed here." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/static.py:42 +#, python-format +msgid "“%(path)s” does not exist" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/static.py:80 +#, python-format +msgid "Index of %(directory)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:7 +msgid "Django: the Web framework for perfectionists with deadlines." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:346 +#, python-format +msgid "" +"View release notes for Django %(version)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:368 +msgid "The install worked successfully! Congratulations!" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:369 +#, python-format +msgid "" +"You are seeing this page because DEBUG=True is in your settings file and you have not configured any " +"URLs." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:384 +msgid "Django Documentation" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:385 +msgid "Topics, references, & how-to’s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:396 +msgid "Tutorial: A Polling App" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:397 +msgid "Get started with Django" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:408 +#, fuzzy +#| msgid "Community" +msgid "Django Community" +msgstr "Communauté" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:409 +msgid "Connect, get help, or contribute" +msgstr "" + +#: venv3/lib/python3.8/site-packages/kombu/transport/qpid.py:1301 +#, python-format +msgid "Attempting to connect to qpid with SASL mechanism %s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/kombu/transport/qpid.py:1306 +#, python-format +msgid "Connected to qpid with SASL mechanism %s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/kombu/transport/qpid.py:1324 +#, python-format +msgid "Unable to connect to qpid with SASL mechanism %s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:371 +msgid "1 second ago" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:377 +msgid "1 minute ago" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:382 +msgid "1 hour ago" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:385 +#, python-format +msgid "%(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:387 +msgid "yesterday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:387 +#, python-format +msgid "yesterday at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:389 +#, python-format +msgid "%(weekday)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:389 +#, python-format +msgid "%(weekday)s at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:392 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:445 +#, python-format +msgid "%(month_name)s %(day)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:394 +#, python-format +msgid "%(month_name)s %(day)s at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:399 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:401 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:439 +#, python-format +msgid "%(weekday)s, %(month_name)s %(day)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:462 +#, python-format +msgid "%(commas)s and %(last)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:68 +msgctxt "law" +msgid "right" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:69 +msgctxt "good" +msgid "right" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:71 +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:74 +msgctxt "organization" +msgid "club" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:76 +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:77 +msgctxt "stick" +msgid "club" +msgstr "" + +#~ msgid "Deactivate user" +#~ msgstr "Désactiver le compte" + +#~ msgid "Reactivate user" +#~ msgstr "Réactiver le compte" + +#, python-format +#~ msgid "%(rating)s star" +#~ msgid_plural "%(rating)s stars" +#~ msgstr[0] "%(rating)s étoile" +#~ msgstr[1] "%(rating)s étoiles" + +#, python-format +#~ msgid "" +#~ "replied to %(username)s's review" +#~ msgstr "" +#~ "a répondu à la critique de %(username)s" + +#, python-format +#~ msgid "" +#~ "replied to %(username)s's comment" +#~ msgstr "" +#~ "a répondu au commentaire de %(username)s" + +#, python-format +#~ msgid "" +#~ "replied to %(username)s's quote" +#~ msgstr "" +#~ "a répondu à la citation de %(username)s" + +#~ msgid "Remove tag" +#~ msgstr "Supprimer le tag" + +#~ msgid "Add tag" +#~ msgstr "Ajouter un tag" + +#, python-format +#~ msgid "Books tagged \"%(tag.name)s\"" +#~ msgstr "Livres tagués « %(tag.name)s »" + #, fuzzy #~| msgid "Started" #~ msgid "Getting Started" @@ -2747,9 +4400,6 @@ msgstr "Un lien de réinitialisation a été envoyé à %s." #~ msgid "Tags" #~ msgstr "Tags" -#~ msgid "Your shelves" -#~ msgstr "Vos étagères" - #~ msgid "Your lists" #~ msgstr "Vos listes" @@ -2791,119 +4441,6 @@ msgstr "Un lien de réinitialisation a été envoyé à %s." #~ msgid "Send follow request" #~ msgstr "Envoyer une demande d’abonnement" -#, fuzzy -#~| msgid "All messages" -#~ msgid "Messages" -#~ msgstr "Tous les messages" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "Enter a valid email address." -#~ msgstr "Adresse email :" - -#, fuzzy -#~| msgid "Series number:" -#~ msgid "Enter a number." -#~ msgstr "Numéro dans la série :" - -#, fuzzy -#~| msgid "Series number:" -#~ msgid "Decimal number" -#~ msgstr "Numéro dans la série :" - -#, fuzzy -#~| msgid "List curation:" -#~ msgid "Duration" -#~ msgstr "Modération de la liste :" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "Email address" -#~ msgstr "Adresse email :" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "IPv4 address" -#~ msgstr "Adresse email :" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "IP address" -#~ msgstr "Adresse email :" - -#, fuzzy -#~| msgid "No active invites" -#~ msgid "Positive integer" -#~ msgstr "Aucune invitation active" - -#, fuzzy -#~| msgid "Images" -#~ msgid "Image" -#~ msgstr "Images" - -#, fuzzy -#~| msgid "Relationships" -#~ msgid "One-to-one relationship" -#~ msgstr "Relations" - -#, fuzzy -#~| msgid "This shelf is empty." -#~ msgid "This field is required." -#~ msgstr "Cette étagère est vide" - -#, fuzzy -#~| msgid "This shelf is empty." -#~ msgid "The submitted file is empty." -#~ msgstr "Cette étagère est vide" - -#, fuzzy -#~| msgid "Started reading" -#~ msgid "Currently" -#~ msgstr "Commencer la lecture" - -#, fuzzy -#~| msgid "Change shelf" -#~ msgid "Change" -#~ msgstr "Changer d’étagère" - -#, fuzzy -#~| msgid "Status" -#~ msgid "Sat" -#~ msgstr "Statut" - -#, fuzzy -#~| msgid "Search" -#~ msgid "March" -#~ msgstr "Chercher" - -#, fuzzy -#~| msgid "Series number:" -#~ msgid "September" -#~ msgstr "Numéro dans la série :" - -#, fuzzy -#~| msgid "Search" -#~ msgctxt "abbrev. month" -#~ msgid "March" -#~ msgstr "Chercher" - -#, fuzzy -#~| msgid "Search" -#~ msgctxt "alt. month" -#~ msgid "March" -#~ msgstr "Chercher" - -#, fuzzy -#~| msgid "Series number:" -#~ msgctxt "alt. month" -#~ msgid "September" -#~ msgstr "Numéro dans la série :" - -#, fuzzy -#~| msgid "No books found matching the query \"%(query)s\"" -#~ msgid "No %(verbose_name)s found matching the query" -#~ msgstr "Aucun livre trouvé pour la requête « %(query)s »" - #~ msgid "Announcements" #~ msgstr "Annonces" diff --git a/locale/zh_CN/LC_MESSAGES/django.po b/locale/zh_CN/LC_MESSAGES/django.po deleted file mode 100644 index 66a26636..00000000 --- a/locale/zh_CN/LC_MESSAGES/django.po +++ /dev/null @@ -1,2776 +0,0 @@ -# Simplified Chinese language text for the BookWyrm UI -# Copyright (C) 2021 Mouse Reeve -# This file is distributed under the same license as the bookwyrm package. -# Mouse Reeve , 2021. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: 0.1.1\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-04-04 04:17+0000\n" -"PO-Revision-Date: 2021-03-20 00:56+0000\n" -"Last-Translator: Kana \n" -"Language-Team: Mouse Reeve \n" -"Language: zh_CN\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=1; plural=0;\n" - -#: bookwyrm/forms.py:226 -msgid "A user with this email already exists." -msgstr "已经存在使用该邮箱的用户。" - -#: bookwyrm/forms.py:240 -msgid "One Day" -msgstr "一天" - -#: bookwyrm/forms.py:241 -msgid "One Week" -msgstr "一周" - -#: bookwyrm/forms.py:242 -msgid "One Month" -msgstr "一个月" - -#: bookwyrm/forms.py:243 -msgid "Does Not Expire" -msgstr "永不失效" - -#: bookwyrm/forms.py:248 -#, python-format -msgid "%(count)d uses" -msgstr "%(count)d 次使用" - -#: bookwyrm/forms.py:251 -msgid "Unlimited" -msgstr "不受限" - -#: bookwyrm/models/fields.py:24 -#, python-format -msgid "%(value)s is not a valid remote_id" -msgstr "%(value)s 不是有效的 remote_id" - -#: bookwyrm/models/fields.py:33 bookwyrm/models/fields.py:42 -#, python-format -msgid "%(value)s is not a valid username" -msgstr "%(value)s 不是有效的用户名" - -#: bookwyrm/models/fields.py:165 bookwyrm/templates/layout.html:152 -msgid "username" -msgstr "用户名" - -#: bookwyrm/models/fields.py:170 -msgid "A user with that username already exists." -msgstr "已经存在使用该用户名的用户。" - -#: bookwyrm/settings.py:150 -msgid "English" -msgstr "English(英语)" - -#: bookwyrm/settings.py:151 -msgid "German" -msgstr "Deutsch(德语)" - -#: bookwyrm/settings.py:152 -msgid "Spanish" -msgstr "Español(西班牙语)" - -#: bookwyrm/settings.py:153 -msgid "French" -msgstr "Français(法语)" - -#: bookwyrm/settings.py:154 -msgid "Simplified Chinese" -msgstr "简体中文" - -#: bookwyrm/templates/404.html:4 bookwyrm/templates/404.html:8 -msgid "Not Found" -msgstr "未找到" - -#: bookwyrm/templates/404.html:9 -msgid "The page you requested doesn't seem to exist!" -msgstr "你请求的页面似乎并不存在!" - -#: bookwyrm/templates/500.html:4 -msgid "Oops!" -msgstr "哎呀!" - -#: bookwyrm/templates/500.html:8 -msgid "Server Error" -msgstr "服务器错误" - -#: bookwyrm/templates/500.html:9 -msgid "Something went wrong! Sorry about that." -msgstr "某些东西出错了!对不起啦。" - -#: bookwyrm/templates/author.html:16 bookwyrm/templates/author.html:17 -msgid "Edit Author" -msgstr "编辑作者" - -#: bookwyrm/templates/author.html:32 -msgid "Wikipedia" -msgstr "维基百科" - -#: bookwyrm/templates/author.html:37 -#, python-format -msgid "Books by %(name)s" -msgstr "%(name)s 所著的书" - -#: bookwyrm/templates/book/book.html:21 -#: bookwyrm/templates/discover/large-book.html:12 -#: bookwyrm/templates/discover/small-book.html:9 -msgid "by" -msgstr "作者" - -#: bookwyrm/templates/book/book.html:29 bookwyrm/templates/book/book.html:30 -msgid "Edit Book" -msgstr "编辑书目" - -#: bookwyrm/templates/book/book.html:49 -#: bookwyrm/templates/book/cover_modal.html:5 -msgid "Add cover" -msgstr "添加封面" - -#: bookwyrm/templates/book/book.html:53 -msgid "Failed to load cover" -msgstr "加载封面失败" - -#: bookwyrm/templates/book/book.html:62 -msgid "ISBN:" -msgstr "ISBN:" - -#: bookwyrm/templates/book/book.html:69 -#: bookwyrm/templates/book/edit_book.html:211 -msgid "OCLC Number:" -msgstr "OCLC 号:" - -#: bookwyrm/templates/book/book.html:76 -#: bookwyrm/templates/book/edit_book.html:215 -msgid "ASIN:" -msgstr "ASIN:" - -#: bookwyrm/templates/book/book.html:85 -msgid "View on OpenLibrary" -msgstr "在 OpenLibrary 查看" - -#: bookwyrm/templates/book/book.html:94 -#, python-format -msgid "(%(review_count)s review)" -msgid_plural "(%(review_count)s reviews)" -msgstr[0] "(%(review_count)s 则书评)" - -#: bookwyrm/templates/book/book.html:100 -msgid "Add Description" -msgstr "添加描述" - -#: bookwyrm/templates/book/book.html:107 -#: bookwyrm/templates/book/edit_book.html:101 -#: bookwyrm/templates/lists/form.html:12 -msgid "Description:" -msgstr "描述:" - -#: bookwyrm/templates/book/book.html:111 -#: bookwyrm/templates/book/edit_book.html:225 -#: bookwyrm/templates/edit_author.html:78 bookwyrm/templates/lists/form.html:42 -#: bookwyrm/templates/preferences/edit_user.html:70 -#: bookwyrm/templates/settings/site.html:93 -#: bookwyrm/templates/snippets/readthrough.html:75 -#: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:42 -#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:42 -#: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:34 -msgid "Save" -msgstr "保存" - -#: bookwyrm/templates/book/book.html:112 bookwyrm/templates/book/book.html:161 -#: bookwyrm/templates/book/cover_modal.html:32 -#: bookwyrm/templates/book/edit_book.html:226 -#: bookwyrm/templates/edit_author.html:79 -#: bookwyrm/templates/moderation/report_modal.html:32 -#: bookwyrm/templates/snippets/delete_readthrough_modal.html:17 -#: bookwyrm/templates/snippets/goal_form.html:32 -#: bookwyrm/templates/snippets/readthrough.html:76 -#: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:43 -#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:43 -#: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:35 -#: bookwyrm/templates/snippets/shelve_button/want_to_read_modal.html:28 -msgid "Cancel" -msgstr "取消" - -#: bookwyrm/templates/book/book.html:121 -#, python-format -msgid "%(count)s editions" -msgstr "%(count)s 个版本" - -#: bookwyrm/templates/book/book.html:129 -#, python-format -msgid "This edition is on your %(shelf_name)s shelf." -msgstr "此版本在你的 %(shelf_name)s 书架上。" - -#: bookwyrm/templates/book/book.html:135 -#, python-format -msgid "A different edition of this book is on your %(shelf_name)s shelf." -msgstr "本书的 另一个版本 在你的 %(shelf_name)s 书架上。" - -#: bookwyrm/templates/book/book.html:144 -msgid "Your reading activity" -msgstr "你的阅读活动" - -#: bookwyrm/templates/book/book.html:146 -msgid "Add read dates" -msgstr "添加阅读日期" - -#: bookwyrm/templates/book/book.html:151 -msgid "You don't have any reading activity for this book." -msgstr "你还没有任何这本书的阅读活动。" - -#: bookwyrm/templates/book/book.html:158 -msgid "Create" -msgstr "创建" - -#: bookwyrm/templates/book/book.html:180 -msgid "Subjects" -msgstr "主题" - -#: bookwyrm/templates/book/book.html:191 -msgid "Places" -msgstr "地点" - -#: bookwyrm/templates/book/book.html:202 bookwyrm/templates/layout.html:64 -#: bookwyrm/templates/lists/lists.html:5 bookwyrm/templates/lists/lists.html:12 -#: bookwyrm/templates/search_results.html:91 -#: bookwyrm/templates/user/user_layout.html:62 -msgid "Lists" -msgstr "列表" - -#: bookwyrm/templates/book/book.html:213 -msgid "Add to list" -msgstr "添加到列表" - -#: bookwyrm/templates/book/book.html:223 -#: bookwyrm/templates/book/cover_modal.html:31 -#: bookwyrm/templates/lists/list.html:90 -msgid "Add" -msgstr "添加" - -#: bookwyrm/templates/book/book.html:251 -msgid "rated it" -msgstr "评价了" - -#: bookwyrm/templates/book/cover_modal.html:17 -#: bookwyrm/templates/book/edit_book.html:163 -msgid "Upload cover:" -msgstr "上传封面:" - -#: bookwyrm/templates/book/cover_modal.html:23 -#: bookwyrm/templates/book/edit_book.html:169 -msgid "Load cover from url:" -msgstr "从网址加载封面:" - -#: bookwyrm/templates/book/edit_book.html:5 -#: bookwyrm/templates/book/edit_book.html:11 -#, python-format -msgid "Edit \"%(book_title)s\"" -msgstr "编辑 \"%(book_title)s\"" - -#: bookwyrm/templates/book/edit_book.html:5 -#: bookwyrm/templates/book/edit_book.html:13 -msgid "Add Book" -msgstr "添加书目" - -#: bookwyrm/templates/book/edit_book.html:18 -#: bookwyrm/templates/edit_author.html:13 -msgid "Added:" -msgstr "添加了:" - -#: bookwyrm/templates/book/edit_book.html:19 -#: bookwyrm/templates/edit_author.html:14 -msgid "Updated:" -msgstr "更新了:" - -#: bookwyrm/templates/book/edit_book.html:20 -#: bookwyrm/templates/edit_author.html:15 -msgid "Last edited by:" -msgstr "最后编辑人:" - -#: bookwyrm/templates/book/edit_book.html:40 -msgid "Confirm Book Info" -msgstr "确认书目信息" - -#: bookwyrm/templates/book/edit_book.html:47 -#, python-format -msgid "Is \"%(name)s\" an existing author?" -msgstr "\"%(name)s\" 是已存在的作者吗?" - -#: bookwyrm/templates/book/edit_book.html:52 -#, python-format -msgid "Author of %(book_title)s" -msgstr "%(book_title)s 的作者" - -#: bookwyrm/templates/book/edit_book.html:55 -msgid "This is a new author" -msgstr "这是一位新的作者" - -#: bookwyrm/templates/book/edit_book.html:61 -#, python-format -msgid "Creating a new author: %(name)s" -msgstr "正在创建新的作者: %(name)s" - -#: bookwyrm/templates/book/edit_book.html:67 -msgid "Is this an edition of an existing work?" -msgstr "这是已存在的作品的一个版本吗?" - -#: bookwyrm/templates/book/edit_book.html:71 -msgid "This is a new work" -msgstr "这是一个新的作品。" - -#: bookwyrm/templates/book/edit_book.html:77 -#: bookwyrm/templates/password_reset.html:30 -msgid "Confirm" -msgstr "确认" - -#: bookwyrm/templates/book/edit_book.html:79 -#: bookwyrm/templates/feed/status.html:8 -msgid "Back" -msgstr "返回" - -#: bookwyrm/templates/book/edit_book.html:90 -#: bookwyrm/templates/edit_author.html:31 -msgid "Metadata" -msgstr "元数据" - -#: bookwyrm/templates/book/edit_book.html:91 -msgid "Title:" -msgstr "标题:" - -#: bookwyrm/templates/book/edit_book.html:96 -msgid "Subtitle:" -msgstr "副标题:" - -#: bookwyrm/templates/book/edit_book.html:106 -msgid "Series:" -msgstr "系列:" - -#: bookwyrm/templates/book/edit_book.html:111 -msgid "Series number:" -msgstr "系列编号:" - -#: bookwyrm/templates/book/edit_book.html:117 -msgid "Publisher:" -msgstr "出版社:" - -#: bookwyrm/templates/book/edit_book.html:119 -msgid "Separate multiple publishers with commas." -msgstr "请用英文逗号(,)分开多个出版社。" - -#: bookwyrm/templates/book/edit_book.html:125 -msgid "First published date:" -msgstr "初版时间:" - -#: bookwyrm/templates/book/edit_book.html:130 -msgid "Published date:" -msgstr "出版时间:" - -#: bookwyrm/templates/book/edit_book.html:137 -msgid "Authors" -msgstr "作者" - -#: bookwyrm/templates/book/edit_book.html:143 -#, python-format -msgid "Remove %(name)s" -msgstr "移除 %(name)s" - -#: bookwyrm/templates/book/edit_book.html:148 -msgid "Add Authors:" -msgstr "添加作者:" - -#: bookwyrm/templates/book/edit_book.html:149 -msgid "John Doe, Jane Smith" -msgstr "张三, 李四" - -#: bookwyrm/templates/book/edit_book.html:155 -#: bookwyrm/templates/user/shelf.html:75 -msgid "Cover" -msgstr "封面" - -#: bookwyrm/templates/book/edit_book.html:182 -msgid "Physical Properties" -msgstr "实体性质" - -#: bookwyrm/templates/book/edit_book.html:183 -#: bookwyrm/templates/book/format_filter.html:5 -msgid "Format:" -msgstr "格式:" - -#: bookwyrm/templates/book/edit_book.html:191 -msgid "Pages:" -msgstr "页数:" - -#: bookwyrm/templates/book/edit_book.html:198 -msgid "Book Identifiers" -msgstr "书目标识号" - -#: bookwyrm/templates/book/edit_book.html:199 -msgid "ISBN 13:" -msgstr "ISBN 13:" - -#: bookwyrm/templates/book/edit_book.html:203 -msgid "ISBN 10:" -msgstr "ISBN 10:" - -#: bookwyrm/templates/book/edit_book.html:207 -#: bookwyrm/templates/edit_author.html:59 -msgid "Openlibrary key:" -msgstr "Openlibrary key:" - -#: bookwyrm/templates/book/editions.html:5 -#, python-format -msgid "Editions of %(book_title)s" -msgstr "%(book_title)s 的各版本" - -#: bookwyrm/templates/book/editions.html:9 -#, python-format -msgid "Editions of \"%(work_title)s\"" -msgstr "\"%(work_title)s\" 的各版本" - -#: bookwyrm/templates/book/format_filter.html:8 -#: bookwyrm/templates/book/language_filter.html:8 -msgid "Any" -msgstr "所有" - -#: bookwyrm/templates/book/language_filter.html:5 -msgid "Language:" -msgstr "语言:" - -#: bookwyrm/templates/book/publisher_info.html:6 -#, python-format -msgid "%(format)s, %(pages)s pages" -msgstr "%(format)s, %(pages)s 页" - -#: bookwyrm/templates/book/publisher_info.html:8 -#, python-format -msgid "%(pages)s pages" -msgstr "%(pages)s 页" - -#: bookwyrm/templates/book/publisher_info.html:13 -#, python-format -msgid "%(languages)s language" -msgstr "%(languages)s 语言" - -#: bookwyrm/templates/book/publisher_info.html:18 -#, python-format -msgid "Published %(date)s by %(publisher)s." -msgstr "在 %(date)s 由 %(publisher)s 出版。" - -#: bookwyrm/templates/book/publisher_info.html:20 -#, python-format -msgid "Published %(date)s" -msgstr "于 %(date)s 出版" - -#: bookwyrm/templates/book/publisher_info.html:22 -#, python-format -msgid "Published by %(publisher)s." -msgstr "由 %(publisher)s 出版。" - -#: bookwyrm/templates/components/inline_form.html:8 -#: bookwyrm/templates/components/modal.html:11 -#: bookwyrm/templates/feed/feed_layout.html:57 -#: bookwyrm/templates/get_started/layout.html:19 -#: bookwyrm/templates/get_started/layout.html:52 -msgid "Close" -msgstr "关闭" - -#: bookwyrm/templates/directory/community_filter.html:5 -msgid "Community" -msgstr "社区" - -#: bookwyrm/templates/directory/community_filter.html:8 -msgid "Local users" -msgstr "本地用户" - -#: bookwyrm/templates/directory/community_filter.html:12 -msgid "Federated community" -msgstr "跨站社区" - -#: bookwyrm/templates/directory/directory.html:6 -#: bookwyrm/templates/directory/directory.html:11 -#: bookwyrm/templates/layout.html:92 -msgid "Directory" -msgstr "目录" - -#: bookwyrm/templates/directory/directory.html:19 -msgid "Make your profile discoverable to other BookWyrm users." -msgstr "使你的个人资料可以被其它 BookWyrm 用户发现。" - -#: bookwyrm/templates/directory/directory.html:26 -#, python-format -msgid "You can opt-out at any time in your profile settings." -msgstr "你可以在任何时候从你的 个人资料设定 中退出。" - -#: bookwyrm/templates/directory/directory.html:31 -#: bookwyrm/templates/snippets/goal_card.html:22 -msgid "Dismiss message" -msgstr "遣散消息" - -#: bookwyrm/templates/directory/directory.html:71 -msgid "follower you follow" -msgid_plural "followers you follow" -msgstr[0] "你关注的关注者" - -#: bookwyrm/templates/directory/directory.html:78 -msgid "book on your shelves" -msgid_plural "books on your shelves" -msgstr[0] "你书架上的书" - -#: bookwyrm/templates/directory/directory.html:86 -msgid "posts" -msgstr "发文" - -#: bookwyrm/templates/directory/directory.html:92 -msgid "last active" -msgstr "最后活跃" - -#: bookwyrm/templates/directory/sort_filter.html:5 -msgid "Order by" -msgstr "排列顺序" - -#: bookwyrm/templates/directory/sort_filter.html:8 -msgid "Suggested" -msgstr "受推荐" - -#: bookwyrm/templates/directory/sort_filter.html:9 -msgid "Recently active" -msgstr "最近活跃" - -#: bookwyrm/templates/directory/user_type_filter.html:5 -msgid "User type" -msgstr "用户类型" - -#: bookwyrm/templates/directory/user_type_filter.html:8 -msgid "BookWyrm users" -msgstr "BookWyrm 用户" - -#: bookwyrm/templates/directory/user_type_filter.html:12 -msgid "All known users" -msgstr "所有已知用户" - -#: bookwyrm/templates/discover/about.html:7 -#, python-format -msgid "About %(site_name)s" -msgstr "关于 %(site_name)s" - -#: bookwyrm/templates/discover/about.html:10 -#: bookwyrm/templates/discover/about.html:20 -msgid "Code of Conduct" -msgstr "行为准则" - -#: bookwyrm/templates/discover/about.html:13 -#: bookwyrm/templates/discover/about.html:29 -msgid "Privacy Policy" -msgstr "隐私政策" - -#: bookwyrm/templates/discover/discover.html:6 -msgid "Recent Books" -msgstr "最近书目" - -#: bookwyrm/templates/discover/landing_layout.html:5 -#: bookwyrm/templates/get_started/layout.html:4 -msgid "Welcome" -msgstr "欢迎" - -#: bookwyrm/templates/discover/landing_layout.html:17 -msgid "Decentralized" -msgstr "去中心化" - -#: bookwyrm/templates/discover/landing_layout.html:23 -msgid "Friendly" -msgstr "友好" - -#: bookwyrm/templates/discover/landing_layout.html:29 -msgid "Anti-Corporate" -msgstr "反企业" - -#: bookwyrm/templates/discover/landing_layout.html:44 -#, python-format -msgid "Join %(name)s" -msgstr "加入 %(name)s" - -#: bookwyrm/templates/discover/landing_layout.html:51 -#: bookwyrm/templates/login.html:48 -msgid "This instance is closed" -msgstr "本实例不开放。" - -#: bookwyrm/templates/discover/landing_layout.html:57 -msgid "Thank you! Your request has been received." -msgstr "谢谢你!我们已经受到了你的请求。" - -#: bookwyrm/templates/discover/landing_layout.html:60 -msgid "Request an Invitation" -msgstr "请求邀请" - -#: bookwyrm/templates/discover/landing_layout.html:64 -#: bookwyrm/templates/password_reset_request.html:18 -#: bookwyrm/templates/preferences/edit_user.html:38 -#: bookwyrm/templates/snippets/register_form.html:13 -msgid "Email address:" -msgstr "邮箱地址:" - -#: bookwyrm/templates/discover/landing_layout.html:70 -#: bookwyrm/templates/moderation/report_modal.html:31 -msgid "Submit" -msgstr "提交" - -#: bookwyrm/templates/discover/landing_layout.html:79 -msgid "Your Account" -msgstr "你的帐号" - -#: bookwyrm/templates/edit_author.html:5 -msgid "Edit Author:" -msgstr "编辑作者:" - -#: bookwyrm/templates/edit_author.html:32 bookwyrm/templates/lists/form.html:8 -#: bookwyrm/templates/user/create_shelf_form.html:13 -#: bookwyrm/templates/user/edit_shelf_form.html:14 -msgid "Name:" -msgstr "名称:" - -#: bookwyrm/templates/edit_author.html:37 -msgid "Bio:" -msgstr "简介:" - -#: bookwyrm/templates/edit_author.html:42 -msgid "Wikipedia link:" -msgstr "维基百科链接:" - -#: bookwyrm/templates/edit_author.html:47 -msgid "Birth date:" -msgstr "出生日期:" - -#: bookwyrm/templates/edit_author.html:52 -msgid "Death date:" -msgstr "死亡日期:" - -#: bookwyrm/templates/edit_author.html:58 -msgid "Author Identifiers" -msgstr "作者标识号:" - -#: bookwyrm/templates/edit_author.html:64 -msgid "Librarything key:" -msgstr "Librarything key:" - -#: bookwyrm/templates/edit_author.html:69 -msgid "Goodreads key:" -msgstr "Goodreads key:" - -#: bookwyrm/templates/email/html_layout.html:15 -#: bookwyrm/templates/email/text_layout.html:2 -msgid "Hi there," -msgstr "你好呀," - -#: bookwyrm/templates/email/html_layout.html:21 -#, python-format -msgid "BookWyrm hosted on %(site_name)s" -msgstr "位于 %(site_name)s 的 BookWyrm" - -#: bookwyrm/templates/email/html_layout.html:23 -msgid "Email preference" -msgstr "邮箱偏好" - -#: bookwyrm/templates/email/invite/html_content.html:6 -#: bookwyrm/templates/email/invite/subject.html:2 -#, python-format -msgid "You're invited to join %(site_name)s!" -msgstr "你受邀请加入 %(site_name)s!" - -#: bookwyrm/templates/email/invite/html_content.html:9 -msgid "Join Now" -msgstr "立即加入" - -#: bookwyrm/templates/email/invite/html_content.html:15 -#, python-format -msgid "Learn more about this instance." -msgstr "了解更多 有关本实例的信息。" - -#: bookwyrm/templates/email/invite/text_content.html:4 -#, python-format -msgid "You're invited to join %(site_name)s! Click the link below to create an account." -msgstr "你受邀请加入 %(site_name)s!点击下面的连接来创建帐号。" - -#: bookwyrm/templates/email/invite/text_content.html:8 -msgid "Learn more about this instance:" -msgstr "了解更多有关本实例的信息:" - -#: bookwyrm/templates/email/password_reset/html_content.html:6 -#: bookwyrm/templates/email/password_reset/text_content.html:4 -#, python-format -msgid "You requested to reset your %(site_name)s password. Click the link below to set a new password and log in to your account." -msgstr "你请求重置你在 %(site_name)s 的密码。点击下面的链接来设置新密码并登录你的帐号。" - -#: bookwyrm/templates/email/password_reset/html_content.html:9 -#: bookwyrm/templates/password_reset.html:4 -#: bookwyrm/templates/password_reset.html:10 -#: bookwyrm/templates/password_reset_request.html:4 -#: bookwyrm/templates/password_reset_request.html:10 -msgid "Reset Password" -msgstr "重设密码" - -#: bookwyrm/templates/email/password_reset/html_content.html:13 -#: bookwyrm/templates/email/password_reset/text_content.html:8 -msgid "If you didn't request to reset your password, you can ignore this email." -msgstr "如果你没有请求重设密码,你可以忽略这封邮件。" - -#: bookwyrm/templates/email/password_reset/subject.html:2 -#, python-format -msgid "Reset your %(site_name)s password" -msgstr "重置你在 %(site_name)s 的密码" - -#: bookwyrm/templates/feed/direct_messages.html:8 -#, python-format -msgid "Direct Messages with %(username)s" -msgstr "与 %(username)s 私信" - -#: bookwyrm/templates/feed/direct_messages.html:10 -#: bookwyrm/templates/layout.html:87 -msgid "Direct Messages" -msgstr "私信" - -#: bookwyrm/templates/feed/direct_messages.html:13 -msgid "All messages" -msgstr "所有消息" - -#: bookwyrm/templates/feed/direct_messages.html:22 -msgid "You have no messages right now." -msgstr "你现在没有消息。" - -#: bookwyrm/templates/feed/feed.html:9 -msgid "Home Timeline" -msgstr "主页时间线" - -#: bookwyrm/templates/feed/feed.html:11 -msgid "Local Timeline" -msgstr "本地时间线" - -#: bookwyrm/templates/feed/feed.html:13 -msgid "Federated Timeline" -msgstr "跨站时间线" - -#: bookwyrm/templates/feed/feed.html:19 -msgid "Home" -msgstr "主页" - -#: bookwyrm/templates/feed/feed.html:22 -msgid "Local" -msgstr "本站" - -#: bookwyrm/templates/feed/feed.html:25 -msgid "Federated" -msgstr "跨站" - -#: bookwyrm/templates/feed/feed.html:33 -#, python-format -msgid "load 0 unread status(es)" -msgstr "加载 0 条未读状态" - -#: bookwyrm/templates/feed/feed.html:48 -msgid "There aren't any activities right now! Try following a user to get started" -msgstr "现在还没有任何活动!尝试着从关注一个用户开始吧" - -#: bookwyrm/templates/feed/feed.html:56 -#: bookwyrm/templates/get_started/users.html:6 -msgid "Who to follow" -msgstr "可以关注的人" - -#: bookwyrm/templates/feed/feed_layout.html:5 -msgid "Updates" -msgstr "更新" - -#: bookwyrm/templates/feed/feed_layout.html:11 -#: bookwyrm/templates/layout.html:58 -#: bookwyrm/templates/user/books_header.html:3 -msgid "Your books" -msgstr "你的书目" - -#: bookwyrm/templates/feed/feed_layout.html:13 -msgid "There are no books here right now! Try searching for a book to get started" -msgstr "现在这里还没有任何书目!尝试着从搜索某本书开始吧" - -#: bookwyrm/templates/feed/feed_layout.html:23 -#: bookwyrm/templates/user/shelf.html:28 -msgid "To Read" -msgstr "想读" - -#: bookwyrm/templates/feed/feed_layout.html:24 -#: bookwyrm/templates/user/shelf.html:28 -msgid "Currently Reading" -msgstr "在读" - -#: bookwyrm/templates/feed/feed_layout.html:25 -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:11 -#: bookwyrm/templates/user/shelf.html:28 -msgid "Read" -msgstr "读过" - -#: bookwyrm/templates/feed/feed_layout.html:74 bookwyrm/templates/goal.html:26 -#: bookwyrm/templates/snippets/goal_card.html:6 -#, python-format -msgid "%(year)s Reading Goal" -msgstr "%(year)s 阅读目标" - -#: bookwyrm/templates/feed/suggested_users.html:16 -#, python-format -msgid "%(mutuals)s follower you follow" -msgid_plural "%(mutuals)s followers you follow" -msgstr[0] "%(mutuals)s 个你也关注的关注者" - -#: bookwyrm/templates/feed/suggested_users.html:19 -#, python-format -msgid "%(shared_books)s book on your shelves" -msgid_plural "%(shared_books)s books on your shelves" -msgstr[0] "%(shared_books)s 本在你书架上也有的书" - -#: bookwyrm/templates/get_started/book_preview.html:6 -#, python-format -msgid "Have you read %(book_title)s?" -msgstr "你读过 %(book_title)s 了吗?" - -#: bookwyrm/templates/get_started/books.html:6 -msgid "What are you reading?" -msgstr "你在阅读什么?" - -#: bookwyrm/templates/get_started/books.html:9 -#: bookwyrm/templates/lists/list.html:58 -msgid "Search for a book" -msgstr "搜索书目" - -#: bookwyrm/templates/get_started/books.html:11 -#: bookwyrm/templates/isbn_search_results.html:17 -#: bookwyrm/templates/search_results.html:17 -#, python-format -msgid "No books found for \"%(query)s\"" -msgstr "没有找到 \"%(query)s\" 的书目" - -#: bookwyrm/templates/get_started/books.html:11 -#, python-format -msgid "You can add books when you start using %(site_name)s." -msgstr "你可以在开始使用 %(site_name)s 后添加书目。" - -#: bookwyrm/templates/get_started/books.html:16 -#: bookwyrm/templates/get_started/books.html:17 -#: bookwyrm/templates/get_started/users.html:18 -#: bookwyrm/templates/get_started/users.html:19 -#: bookwyrm/templates/layout.html:37 bookwyrm/templates/layout.html:38 -#: bookwyrm/templates/lists/list.html:62 -msgid "Search" -msgstr "搜索" - -#: bookwyrm/templates/get_started/books.html:26 -msgid "Suggested Books" -msgstr "推荐的书目" - -#: bookwyrm/templates/get_started/books.html:41 -#, python-format -msgid "Popular on %(site_name)s" -msgstr "%(site_name)s 上的热门" - -#: bookwyrm/templates/get_started/books.html:51 -#: bookwyrm/templates/lists/list.html:75 -msgid "No books found" -msgstr "没有找到书目" - -#: bookwyrm/templates/get_started/books.html:54 -#: bookwyrm/templates/get_started/profile.html:54 -msgid "Save & continue" -msgstr "保存 & 继续" - -#: bookwyrm/templates/get_started/layout.html:14 -#, python-format -msgid "Welcome to %(site_name)s!" -msgstr "欢迎来到 %(site_name)s!" - -#: bookwyrm/templates/get_started/layout.html:16 -msgid "These are some first steps to get you started." -msgstr "这些最初的步骤可以帮助你入门。" - -#: bookwyrm/templates/get_started/layout.html:30 -#: bookwyrm/templates/get_started/profile.html:6 -msgid "Create your profile" -msgstr "创建你的个人资料" - -#: bookwyrm/templates/get_started/layout.html:34 -msgid "Add books" -msgstr "添加书目" - -#: bookwyrm/templates/get_started/layout.html:38 -msgid "Find friends" -msgstr "寻找同好" - -#: bookwyrm/templates/get_started/layout.html:44 -msgid "Skip this step" -msgstr "跳过此步骤" - -#: bookwyrm/templates/get_started/layout.html:48 -msgid "Finish" -msgstr "完成" - -#: bookwyrm/templates/get_started/profile.html:15 -#: bookwyrm/templates/preferences/edit_user.html:24 -msgid "Display name:" -msgstr "显示名称:" - -#: bookwyrm/templates/get_started/profile.html:22 -#: bookwyrm/templates/preferences/edit_user.html:31 -msgid "Summary:" -msgstr "概要:" - -#: bookwyrm/templates/get_started/profile.html:23 -msgid "A little bit about you" -msgstr "少许关于你的信息" - -#: bookwyrm/templates/get_started/profile.html:32 -#: bookwyrm/templates/preferences/edit_user.html:17 -msgid "Avatar:" -msgstr "头像:" - -#: bookwyrm/templates/get_started/profile.html:42 -#: bookwyrm/templates/preferences/edit_user.html:52 -msgid "Manually approve followers:" -msgstr "手动批准关注者:" - -#: bookwyrm/templates/get_started/profile.html:48 -#: bookwyrm/templates/preferences/edit_user.html:58 -msgid "Show this account in suggested users:" -msgstr "在推荐的用户中显示此帐号:" - -#: bookwyrm/templates/get_started/profile.html:52 -msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users." -msgstr "你的帐号会显示在目录中,并且可能会受其它 BookWyrm 用户推荐。" - -#: bookwyrm/templates/get_started/users.html:11 -msgid "Search for a user" -msgstr "搜索用户" - -#: bookwyrm/templates/get_started/users.html:13 -#: bookwyrm/templates/search_results.html:76 -#, python-format -msgid "No users found for \"%(query)s\"" -msgstr "没有找到 \"%(query)s\" 的用户" - -#: bookwyrm/templates/goal.html:7 -#, python-format -msgid "%(year)s Reading Progress" -msgstr "%(year)s 阅读进度" - -#: bookwyrm/templates/goal.html:11 -msgid "Edit Goal" -msgstr "编辑目标" - -#: bookwyrm/templates/goal.html:30 -#: bookwyrm/templates/snippets/goal_card.html:13 -#, python-format -msgid "Set a goal for how many books you'll finish reading in %(year)s, and track your progress throughout the year." -msgstr "设定一个 %(year)s 内要读多少书的目标,并记录你全年的进度。" - -#: bookwyrm/templates/goal.html:39 -#, python-format -msgid "%(name)s hasn't set a reading goal for %(year)s." -msgstr "%(name)s 还没有设定 %(year)s 的阅读目标。" - -#: bookwyrm/templates/goal.html:51 -#, python-format -msgid "Your %(year)s Books" -msgstr "你 %(year)s 的书目" - -#: bookwyrm/templates/goal.html:53 -#, python-format -msgid "%(username)s's %(year)s Books" -msgstr "%(username)s 在 %(year)s 的书目" - -#: bookwyrm/templates/import.html:5 bookwyrm/templates/import.html:9 -#: bookwyrm/templates/layout.html:97 -msgid "Import Books" -msgstr "导入书目" - -#: bookwyrm/templates/import.html:16 -msgid "Data source:" -msgstr "数据来源:" - -#: bookwyrm/templates/import.html:29 -msgid "Data file:" -msgstr "数据文件:" - -#: bookwyrm/templates/import.html:37 -msgid "Include reviews" -msgstr "纳入书评" - -#: bookwyrm/templates/import.html:42 -msgid "Privacy setting for imported reviews:" -msgstr "导入书评的隐私设定" - -#: bookwyrm/templates/import.html:48 -msgid "Import" -msgstr "导入" - -#: bookwyrm/templates/import.html:53 -msgid "Recent Imports" -msgstr "最近的导入" - -#: bookwyrm/templates/import.html:55 -msgid "No recent imports" -msgstr "无最近的导入" - -#: bookwyrm/templates/import_status.html:6 -#: bookwyrm/templates/import_status.html:10 -msgid "Import Status" -msgstr "导入状态" - -#: bookwyrm/templates/import_status.html:13 -msgid "Import started:" -msgstr "导入开始:" - -#: bookwyrm/templates/import_status.html:17 -msgid "Import completed:" -msgstr "导入完成:" - -#: bookwyrm/templates/import_status.html:20 -msgid "TASK FAILED" -msgstr "任务失败" - -#: bookwyrm/templates/import_status.html:26 -msgid "Import still in progress." -msgstr "还在导入中。" - -#: bookwyrm/templates/import_status.html:28 -msgid "(Hit reload to update!)" -msgstr "(按下重新加载来更新!)" - -#: bookwyrm/templates/import_status.html:35 -msgid "Failed to load" -msgstr "加载失败" - -#: bookwyrm/templates/import_status.html:44 -#, python-format -msgid "Jump to the bottom of the list to select the %(failed_count)s items which failed to import." -msgstr "跳转至列表底部来选取 %(failed_count)s 个导入失败的项目。" - -#: bookwyrm/templates/import_status.html:79 -msgid "Select all" -msgstr "全选" - -#: bookwyrm/templates/import_status.html:82 -msgid "Retry items" -msgstr "重试项目" - -#: bookwyrm/templates/import_status.html:108 -msgid "Successfully imported" -msgstr "成功导入了" - -#: bookwyrm/templates/import_status.html:112 -#: bookwyrm/templates/lists/curate.html:14 -msgid "Book" -msgstr "书目" - -#: bookwyrm/templates/import_status.html:115 -#: bookwyrm/templates/snippets/create_status_form.html:10 -#: bookwyrm/templates/user/shelf.html:76 -msgid "Title" -msgstr "标题" - -#: bookwyrm/templates/import_status.html:118 -#: bookwyrm/templates/user/shelf.html:77 -msgid "Author" -msgstr "作者" - -#: bookwyrm/templates/import_status.html:141 -msgid "Imported" -msgstr "已导入" - -#: bookwyrm/templates/invite.html:4 bookwyrm/templates/invite.html:12 -#: bookwyrm/templates/login.html:43 -msgid "Create an Account" -msgstr "创建帐号" - -#: bookwyrm/templates/invite.html:21 -msgid "Permission Denied" -msgstr "没有权限" - -#: bookwyrm/templates/invite.html:22 -msgid "Sorry! This invite code is no longer valid." -msgstr "抱歉!此邀请码已不再有效。" - -#: bookwyrm/templates/isbn_search_results.html:4 -#: bookwyrm/templates/search_results.html:4 -msgid "Search Results" -msgstr "搜索结果" - -#: bookwyrm/templates/isbn_search_results.html:9 -#: bookwyrm/templates/search_results.html:9 -#, python-format -msgid "Search Results for \"%(query)s\"" -msgstr "\"%(query)s\" 的搜索结果" - -#: bookwyrm/templates/isbn_search_results.html:14 -#: bookwyrm/templates/search_results.html:14 -msgid "Matching Books" -msgstr "匹配的书目" - -#: bookwyrm/templates/layout.html:33 -msgid "Search for a book or user" -msgstr "搜索书目或用户" - -#: bookwyrm/templates/layout.html:47 bookwyrm/templates/layout.html:48 -msgid "Main navigation menu" -msgstr "主导航菜单" - -#: bookwyrm/templates/layout.html:61 -msgid "Feed" -msgstr "动态" - -#: bookwyrm/templates/layout.html:102 -msgid "Settings" -msgstr "设置" - -#: bookwyrm/templates/layout.html:111 -#: bookwyrm/templates/settings/admin_layout.html:24 -#: bookwyrm/templates/settings/manage_invite_requests.html:15 -#: bookwyrm/templates/settings/manage_invites.html:3 -#: bookwyrm/templates/settings/manage_invites.html:15 -msgid "Invites" -msgstr "邀请" - -#: bookwyrm/templates/layout.html:118 -msgid "Admin" -msgstr "管理员" - -#: bookwyrm/templates/layout.html:125 -msgid "Log out" -msgstr "登出" - -#: bookwyrm/templates/layout.html:133 bookwyrm/templates/layout.html:134 -#: bookwyrm/templates/notifications.html:6 -#: bookwyrm/templates/notifications.html:10 -msgid "Notifications" -msgstr "通知" - -#: bookwyrm/templates/layout.html:151 bookwyrm/templates/layout.html:155 -#: bookwyrm/templates/login.html:17 -#: bookwyrm/templates/snippets/register_form.html:4 -msgid "Username:" -msgstr "用户名:" - -#: bookwyrm/templates/layout.html:156 -msgid "password" -msgstr "密码" - -#: bookwyrm/templates/layout.html:157 bookwyrm/templates/login.html:36 -msgid "Forgot your password?" -msgstr "忘记了密码?" - -#: bookwyrm/templates/layout.html:160 bookwyrm/templates/login.html:10 -#: bookwyrm/templates/login.html:33 -msgid "Log in" -msgstr "登录" - -#: bookwyrm/templates/layout.html:168 -msgid "Join" -msgstr "加入" - -#: bookwyrm/templates/layout.html:191 -msgid "About this server" -msgstr "关于本服务器" - -#: bookwyrm/templates/layout.html:195 -msgid "Contact site admin" -msgstr "联系站点管理员" - -#: bookwyrm/templates/layout.html:202 -#, python-format -msgid "Support %(site_name)s on %(support_title)s" -msgstr "在 %(support_title)s 上支持 %(site_name)s" - -#: bookwyrm/templates/layout.html:206 -msgid "BookWyrm is open source software. You can contribute or report issues on GitHub." -msgstr "BookWyrm 是开源软件。你可以在 GitHub 贡献或报告问题。" - -#: bookwyrm/templates/lists/create_form.html:5 -#: bookwyrm/templates/lists/lists.html:19 -msgid "Create List" -msgstr "创建列表" - -#: bookwyrm/templates/lists/created_text.html:5 -#, python-format -msgid "Created and curated by %(username)s" -msgstr "由 %(username)s 创建并策展" - -#: bookwyrm/templates/lists/created_text.html:7 -#, python-format -msgid "Created by %(username)s" -msgstr "由 %(username)s 创建" - -#: bookwyrm/templates/lists/curate.html:6 -msgid "Pending Books" -msgstr "等候中的书目" - -#: bookwyrm/templates/lists/curate.html:7 -msgid "Go to list" -msgstr "前往列表" - -#: bookwyrm/templates/lists/curate.html:9 -msgid "You're all set!" -msgstr "都弄好了!" - -#: bookwyrm/templates/lists/curate.html:15 -msgid "Suggested by" -msgstr "推荐来自" - -#: bookwyrm/templates/lists/curate.html:35 -msgid "Approve" -msgstr "批准" - -#: bookwyrm/templates/lists/curate.html:41 -msgid "Discard" -msgstr "削除" - -#: bookwyrm/templates/lists/edit_form.html:5 -#: bookwyrm/templates/lists/list_layout.html:18 -msgid "Edit List" -msgstr "编辑列表" - -#: bookwyrm/templates/lists/form.html:18 -msgid "List curation:" -msgstr "列表策展:" - -#: bookwyrm/templates/lists/form.html:21 -msgid "Closed" -msgstr "已关闭" - -#: bookwyrm/templates/lists/form.html:22 -msgid "Only you can add and remove books to this list" -msgstr "只有你可以在此列表中添加或移除书目" - -#: bookwyrm/templates/lists/form.html:26 -msgid "Curated" -msgstr "策展" - -#: bookwyrm/templates/lists/form.html:27 -msgid "Anyone can suggest books, subject to your approval" -msgstr "任何人都可以推荐书目、主题让你批准" - -#: bookwyrm/templates/lists/form.html:31 -#: bookwyrm/templates/moderation/reports.html:24 -msgid "Open" -msgstr "开放" - -#: bookwyrm/templates/lists/form.html:32 -msgid "Anyone can add books to this list" -msgstr "任何人都可以向此列表中添加书目" - -#: bookwyrm/templates/lists/list.html:17 -msgid "This list is currently empty" -msgstr "此列表当前是空的" - -#: bookwyrm/templates/lists/list.html:35 -#, python-format -msgid "Added by %(username)s" -msgstr "由 %(username)s 添加" - -#: bookwyrm/templates/lists/list.html:41 -#: bookwyrm/templates/snippets/shelf_selector.html:26 -msgid "Remove" -msgstr "移除" - -#: bookwyrm/templates/lists/list.html:54 -msgid "Add Books" -msgstr "添加书目" - -#: bookwyrm/templates/lists/list.html:54 -msgid "Suggest Books" -msgstr "推荐书目" - -#: bookwyrm/templates/lists/list.html:63 -msgid "search" -msgstr "搜索" - -#: bookwyrm/templates/lists/list.html:69 -msgid "Clear search" -msgstr "清除搜索" - -#: bookwyrm/templates/lists/list.html:74 -#, python-format -msgid "No books found matching the query \"%(query)s\"" -msgstr "没有符合 \"%(query)s\" 请求的书目" - -#: bookwyrm/templates/lists/list.html:90 -msgid "Suggest" -msgstr "推荐" - -#: bookwyrm/templates/login.html:4 -msgid "Login" -msgstr "登录" - -#: bookwyrm/templates/login.html:23 bookwyrm/templates/password_reset.html:17 -#: bookwyrm/templates/snippets/register_form.html:22 -msgid "Password:" -msgstr "密码:" - -#: bookwyrm/templates/login.html:49 -msgid "Contact an administrator to get an invite" -msgstr "联系管理员以取得邀请" - -#: bookwyrm/templates/login.html:59 -msgid "More about this site" -msgstr "关于本站点的更多" - -#: bookwyrm/templates/moderation/report.html:5 -#: bookwyrm/templates/moderation/report.html:6 -#: bookwyrm/templates/moderation/report_preview.html:6 -#, python-format -msgid "Report #%(report_id)s: %(username)s" -msgstr "报告 #%(report_id)s: %(username)s" - -#: bookwyrm/templates/moderation/report.html:10 -msgid "Back to reports" -msgstr "回到报告" - -#: bookwyrm/templates/moderation/report.html:18 -msgid "Actions" -msgstr "动作" - -#: bookwyrm/templates/moderation/report.html:19 -msgid "View user profile" -msgstr "查看用户个人资料" - -#: bookwyrm/templates/moderation/report.html:22 -#: bookwyrm/templates/snippets/status/status_options.html:25 -#: bookwyrm/templates/snippets/user_options.html:13 -msgid "Send direct message" -msgstr "发送私信" - -#: bookwyrm/templates/moderation/report.html:27 -msgid "Deactivate user" -msgstr "停用用户" - -#: bookwyrm/templates/moderation/report.html:29 -msgid "Reactivate user" -msgstr "启用用户" - -#: bookwyrm/templates/moderation/report.html:36 -msgid "Moderator Comments" -msgstr "监察员评论" - -#: bookwyrm/templates/moderation/report.html:54 -#: bookwyrm/templates/snippets/create_status.html:12 -#: bookwyrm/templates/snippets/create_status_form.html:52 -msgid "Comment" -msgstr "评论" - -#: bookwyrm/templates/moderation/report.html:59 -msgid "Reported statuses" -msgstr "被报告的状态" - -#: bookwyrm/templates/moderation/report.html:61 -msgid "No statuses reported" -msgstr "没有被报告的状态" - -#: bookwyrm/templates/moderation/report.html:67 -msgid "Statuses has been deleted" -msgstr "状态已被删除" - -#: bookwyrm/templates/moderation/report_modal.html:6 -#, python-format -msgid "Report @%(username)s" -msgstr "报告 %(username)s" - -#: bookwyrm/templates/moderation/report_modal.html:21 -#, python-format -msgid "This report will be sent to %(site_name)s's moderators for review." -msgstr "本报告会被发送至 %(site_name)s 的监察员以复查。" - -#: bookwyrm/templates/moderation/report_modal.html:22 -msgid "More info about this report:" -msgstr "关于本报告的更多信息" - -#: bookwyrm/templates/moderation/report_preview.html:13 -msgid "No notes provided" -msgstr "没有提供摘记" - -#: bookwyrm/templates/moderation/report_preview.html:20 -#, python-format -msgid "Reported by %(username)s" -msgstr "由 %(username)s 报告" - -#: bookwyrm/templates/moderation/report_preview.html:30 -msgid "Re-open" -msgstr "重新开启" - -#: bookwyrm/templates/moderation/report_preview.html:32 -msgid "Resolve" -msgstr "已解决" - -#: bookwyrm/templates/moderation/reports.html:6 -#, python-format -msgid "Reports: %(server_name)s" -msgstr "报告: %(server_name)s" - -#: bookwyrm/templates/moderation/reports.html:8 -#: bookwyrm/templates/moderation/reports.html:16 -#: bookwyrm/templates/settings/admin_layout.html:28 -msgid "Reports" -msgstr "报告" - -#: bookwyrm/templates/moderation/reports.html:13 -#, python-format -msgid "Reports: %(server_name)s" -msgstr "报告: %(server_name)s" - -#: bookwyrm/templates/moderation/reports.html:27 -msgid "Resolved" -msgstr "已解决" - -#: bookwyrm/templates/moderation/reports.html:34 -msgid "No reports found." -msgstr "没有找到报告" - -#: bookwyrm/templates/notifications.html:14 -msgid "Delete notifications" -msgstr "删除通知" - -#: bookwyrm/templates/notifications.html:53 -#, python-format -msgid "favorited your review of %(book_title)s" -msgstr "喜欢了你 %(book_title)s 的书评" - -#: bookwyrm/templates/notifications.html:55 -#, python-format -msgid "favorited your comment on %(book_title)s" -msgstr "喜欢了你 %(book_title)s 的评论" - -#: bookwyrm/templates/notifications.html:57 -#, python-format -msgid "favorited your quote from %(book_title)s" -msgstr "喜欢了你 来自 %(book_title)s 的引用" - -#: bookwyrm/templates/notifications.html:59 -#, python-format -msgid "favorited your status" -msgstr "喜欢了你的 状态" - -#: bookwyrm/templates/notifications.html:64 -#, python-format -msgid "mentioned you in a review of %(book_title)s" -msgstr "在 %(book_title)s 的书评 里提到了你" - -#: bookwyrm/templates/notifications.html:66 -#, python-format -msgid "mentioned you in a comment on %(book_title)s" -msgstr "在 %(book_title)s 的评论 里提到了你" - -#: bookwyrm/templates/notifications.html:68 -#, python-format -msgid "mentioned you in a quote from %(book_title)s" -msgstr "在 %(book_title)s 的引用 中提到了你" - -#: bookwyrm/templates/notifications.html:70 -#, python-format -msgid "mentioned you in a status" -msgstr "在 状态 中提到了你" - -#: bookwyrm/templates/notifications.html:75 -#, python-format -msgid "replied to your review of %(book_title)s" -msgstr "回复 了你的 %(book_title)s 的书评" - -#: bookwyrm/templates/notifications.html:77 -#, python-format -msgid "replied to your comment on %(book_title)s" -msgstr "回复 了你的 %(book_title)s 的评论" - -#: bookwyrm/templates/notifications.html:79 -#, python-format -msgid "replied to your quote from %(book_title)s" -msgstr "回复 了你 %(book_title)s 中的引用" - -#: bookwyrm/templates/notifications.html:81 -#, python-format -msgid "replied to your status" -msgstr "回复 了你的 状态" - -#: bookwyrm/templates/notifications.html:85 -msgid "followed you" -msgstr "关注了你" - -#: bookwyrm/templates/notifications.html:88 -msgid "sent you a follow request" -msgstr "向你发送了关注请求" - -#: bookwyrm/templates/notifications.html:94 -#, python-format -msgid "boosted your review of %(book_title)s" -msgstr "转发了你的 %(book_title)s 的书评" - -#: bookwyrm/templates/notifications.html:96 -#, python-format -msgid "boosted your comment on%(book_title)s" -msgstr "转发了你的 %(book_title)s 的评论" - -#: bookwyrm/templates/notifications.html:98 -#, python-format -msgid "boosted your quote from %(book_title)s" -msgstr "转发了你的 %(book_title)s 的引用" - -#: bookwyrm/templates/notifications.html:100 -#, python-format -msgid "boosted your status" -msgstr "转发了你的 状态" - -#: bookwyrm/templates/notifications.html:104 -#, python-format -msgid " added %(book_title)s to your list \"%(list_name)s\"" -msgstr " 添加了 %(book_title)s 到你的列表 \"%(list_name)s\"" - -#: bookwyrm/templates/notifications.html:106 -#, python-format -msgid " suggested adding %(book_title)s to your list \"%(list_name)s\"" -msgstr " 推荐添加 %(book_title)s 到你的列表 \"%(list_name)s\"" - -#: bookwyrm/templates/notifications.html:110 -#, python-format -msgid "Your import completed." -msgstr "你的 导入 已完成。" - -#: bookwyrm/templates/notifications.html:113 -#, python-format -msgid "A new report needs moderation." -msgstr "有新的 报告 需要仲裁。" - -#: bookwyrm/templates/notifications.html:139 -msgid "You're all caught up!" -msgstr "你什么也没错过!" - -#: bookwyrm/templates/password_reset.html:23 -#: bookwyrm/templates/preferences/change_password.html:18 -msgid "Confirm password:" -msgstr "确认密码:" - -#: bookwyrm/templates/password_reset_request.html:14 -msgid "A link to reset your password will be sent to your email address" -msgstr "重设你的密码的链接将会被发送到你的邮箱地址" - -#: bookwyrm/templates/password_reset_request.html:28 -msgid "Reset password" -msgstr "重设密码" - -#: bookwyrm/templates/preferences/blocks.html:4 -#: bookwyrm/templates/preferences/blocks.html:7 -#: bookwyrm/templates/preferences/preferences_layout.html:23 -msgid "Blocked Users" -msgstr "屏蔽的用户" - -#: bookwyrm/templates/preferences/blocks.html:12 -msgid "No users currently blocked." -msgstr "当前没有被屏蔽的用户。" - -#: bookwyrm/templates/preferences/change_password.html:4 -#: bookwyrm/templates/preferences/change_password.html:7 -#: bookwyrm/templates/preferences/change_password.html:21 -#: bookwyrm/templates/preferences/preferences_layout.html:17 -msgid "Change Password" -msgstr "更改密码" - -#: bookwyrm/templates/preferences/change_password.html:14 -msgid "New password:" -msgstr "新密码:" - -#: bookwyrm/templates/preferences/edit_user.html:4 -#: bookwyrm/templates/preferences/edit_user.html:7 -msgid "Edit Profile" -msgstr "编辑个人资料" - -#: bookwyrm/templates/preferences/edit_user.html:46 -msgid "Show set reading goal prompt in feed:" -msgstr "在消息流中显示设置阅读目标的提示:" - -#: bookwyrm/templates/preferences/edit_user.html:62 -#, python-format -msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users." -msgstr "你的帐号会显示在 目录 中,并可能受其它 BookWyrm 用户推荐。" - -#: bookwyrm/templates/preferences/edit_user.html:65 -msgid "Preferred Timezone: " -msgstr "偏好的时区:" - -#: bookwyrm/templates/preferences/preferences_layout.html:11 -msgid "Account" -msgstr "帐号" - -#: bookwyrm/templates/preferences/preferences_layout.html:14 -msgid "Profile" -msgstr "个人资料" - -#: bookwyrm/templates/preferences/preferences_layout.html:20 -msgid "Relationships" -msgstr "关系" - -#: bookwyrm/templates/search_results.html:33 -msgid "Didn't find what you were looking for?" -msgstr "没有找到你想找的?" - -#: bookwyrm/templates/search_results.html:35 -msgid "Show results from other catalogues" -msgstr "显示其它类别的结果" - -#: bookwyrm/templates/search_results.html:62 -msgid "Hide results from other catalogues" -msgstr "隐藏其它类别的结果" - -#: bookwyrm/templates/search_results.html:74 -msgid "Matching Users" -msgstr "匹配的用户" - -#: bookwyrm/templates/search_results.html:93 -#, python-format -msgid "No lists found for \"%(query)s\"" -msgstr "没有找到 \"%(query)s\" 的列表" - -#: bookwyrm/templates/settings/admin_layout.html:4 -msgid "Administration" -msgstr "管理" - -#: bookwyrm/templates/settings/admin_layout.html:15 -msgid "Manage Users" -msgstr "管理用户" - -#: bookwyrm/templates/settings/admin_layout.html:19 -#: bookwyrm/templates/settings/user_admin.html:3 -#: bookwyrm/templates/settings/user_admin.html:10 -msgid "Users" -msgstr "用户" - -#: bookwyrm/templates/settings/admin_layout.html:32 -#: bookwyrm/templates/settings/federation.html:3 -#: bookwyrm/templates/settings/federation.html:5 -msgid "Federated Servers" -msgstr "互联的服务器" - -#: bookwyrm/templates/settings/admin_layout.html:37 -msgid "Instance Settings" -msgstr "实例设置" - -#: bookwyrm/templates/settings/admin_layout.html:41 -#: bookwyrm/templates/settings/site.html:4 -#: bookwyrm/templates/settings/site.html:6 -msgid "Site Settings" -msgstr "站点设置" - -#: bookwyrm/templates/settings/admin_layout.html:44 -#: bookwyrm/templates/settings/site.html:13 -msgid "Instance Info" -msgstr "实例信息" - -#: bookwyrm/templates/settings/admin_layout.html:45 -#: bookwyrm/templates/settings/site.html:39 -msgid "Images" -msgstr "图像" - -#: bookwyrm/templates/settings/admin_layout.html:46 -#: bookwyrm/templates/settings/site.html:59 -msgid "Footer Content" -msgstr "页脚内容" - -#: bookwyrm/templates/settings/admin_layout.html:47 -#: bookwyrm/templates/settings/site.html:77 -msgid "Registration" -msgstr "注册" - -#: bookwyrm/templates/settings/federated_server.html:7 -msgid "Back to server list" -msgstr "回到服务器列表" - -#: bookwyrm/templates/settings/federated_server.html:12 -msgid "Details" -msgstr "详细" - -#: bookwyrm/templates/settings/federated_server.html:15 -msgid "Software:" -msgstr "软件:" - -#: bookwyrm/templates/settings/federated_server.html:19 -msgid "Version:" -msgstr "版本:" - -#: bookwyrm/templates/settings/federated_server.html:23 -msgid "Status:" -msgstr "状态:" - -#: bookwyrm/templates/settings/federated_server.html:30 -#: bookwyrm/templates/user/user_layout.html:50 -msgid "Activity" -msgstr "活动" - -#: bookwyrm/templates/settings/federated_server.html:33 -msgid "Users:" -msgstr "用户:" - -#: bookwyrm/templates/settings/federated_server.html:36 -#: bookwyrm/templates/settings/federated_server.html:43 -msgid "View all" -msgstr "查看全部" - -#: bookwyrm/templates/settings/federated_server.html:40 -msgid "Reports:" -msgstr "报告:" - -#: bookwyrm/templates/settings/federated_server.html:47 -msgid "Followed by us:" -msgstr "我们关注了的:" - -#: bookwyrm/templates/settings/federated_server.html:53 -msgid "Followed by them:" -msgstr "TA 们关注了的:" - -#: bookwyrm/templates/settings/federated_server.html:59 -msgid "Blocked by us:" -msgstr "我们所屏蔽的:" - -#: bookwyrm/templates/settings/federation.html:13 -msgid "Server name" -msgstr "服务器名称" - -#: bookwyrm/templates/settings/federation.html:17 -msgid "Date federated" -msgstr "跨站日期" - -#: bookwyrm/templates/settings/federation.html:21 -msgid "Software" -msgstr "软件" - -#: bookwyrm/templates/settings/federation.html:24 -#: bookwyrm/templates/settings/manage_invite_requests.html:40 -#: bookwyrm/templates/settings/status_filter.html:5 -#: bookwyrm/templates/settings/user_admin.html:32 -msgid "Status" -msgstr "状态" - -#: bookwyrm/templates/settings/manage_invite_requests.html:4 -#: bookwyrm/templates/settings/manage_invite_requests.html:11 -#: bookwyrm/templates/settings/manage_invite_requests.html:25 -#: bookwyrm/templates/settings/manage_invites.html:11 -msgid "Invite Requests" -msgstr "邀请请求" - -#: bookwyrm/templates/settings/manage_invite_requests.html:23 -msgid "Ignored Invite Requests" -msgstr "已忽略的邀请请求" - -#: bookwyrm/templates/settings/manage_invite_requests.html:35 -msgid "Date" -msgstr "日期" - -#: bookwyrm/templates/settings/manage_invite_requests.html:38 -msgid "Email" -msgstr "邮箱" - -#: bookwyrm/templates/settings/manage_invite_requests.html:43 -msgid "Action" -msgstr "动作" - -#: bookwyrm/templates/settings/manage_invite_requests.html:46 -msgid "No requests" -msgstr "没有请求" - -#: bookwyrm/templates/settings/manage_invite_requests.html:54 -#: bookwyrm/templates/settings/status_filter.html:16 -msgid "Accepted" -msgstr "已接受" - -#: bookwyrm/templates/settings/manage_invite_requests.html:56 -#: bookwyrm/templates/settings/status_filter.html:12 -msgid "Sent" -msgstr "已发送" - -#: bookwyrm/templates/settings/manage_invite_requests.html:58 -#: bookwyrm/templates/settings/status_filter.html:8 -msgid "Requested" -msgstr "已请求" - -#: bookwyrm/templates/settings/manage_invite_requests.html:68 -msgid "Send invite" -msgstr "发送请求" - -#: bookwyrm/templates/settings/manage_invite_requests.html:70 -msgid "Re-send invite" -msgstr "重新发送请求" - -#: bookwyrm/templates/settings/manage_invite_requests.html:90 -msgid "Ignore" -msgstr "忽略" - -#: bookwyrm/templates/settings/manage_invite_requests.html:92 -msgid "Un-ignore" -msgstr "取消忽略" - -#: bookwyrm/templates/settings/manage_invite_requests.html:103 -msgid "Back to pending requests" -msgstr "回到待处理的请求" - -#: bookwyrm/templates/settings/manage_invite_requests.html:105 -msgid "View ignored requests" -msgstr "查看忽略的请求" - -#: bookwyrm/templates/settings/manage_invites.html:21 -msgid "Generate New Invite" -msgstr "生成新的邀请" - -#: bookwyrm/templates/settings/manage_invites.html:27 -msgid "Expiry:" -msgstr "过期:" - -#: bookwyrm/templates/settings/manage_invites.html:33 -msgid "Use limit:" -msgstr "使用限制:" - -#: bookwyrm/templates/settings/manage_invites.html:40 -msgid "Create Invite" -msgstr "创建邀请" - -#: bookwyrm/templates/settings/manage_invites.html:47 -msgid "Link" -msgstr "链接" - -#: bookwyrm/templates/settings/manage_invites.html:48 -msgid "Expires" -msgstr "过期" - -#: bookwyrm/templates/settings/manage_invites.html:49 -msgid "Max uses" -msgstr "最大使用次数" - -#: bookwyrm/templates/settings/manage_invites.html:50 -msgid "Times used" -msgstr "已使用次数" - -#: bookwyrm/templates/settings/manage_invites.html:53 -msgid "No active invites" -msgstr "无有效的邀请" - -#: bookwyrm/templates/settings/site.html:15 -msgid "Instance Name:" -msgstr "实例名称" - -#: bookwyrm/templates/settings/site.html:19 -msgid "Tagline:" -msgstr "标语" - -#: bookwyrm/templates/settings/site.html:23 -msgid "Instance description:" -msgstr "实例描述:" - -#: bookwyrm/templates/settings/site.html:27 -msgid "Code of conduct:" -msgstr "行为准则:" - -#: bookwyrm/templates/settings/site.html:31 -msgid "Privacy Policy:" -msgstr "隐私政策:" - -#: bookwyrm/templates/settings/site.html:42 -msgid "Logo:" -msgstr "图标:" - -#: bookwyrm/templates/settings/site.html:46 -msgid "Logo small:" -msgstr "小号图标:" - -#: bookwyrm/templates/settings/site.html:50 -msgid "Favicon:" -msgstr "Favicon:" - -#: bookwyrm/templates/settings/site.html:61 -msgid "Support link:" -msgstr "支持链接:" - -#: bookwyrm/templates/settings/site.html:65 -msgid "Support title:" -msgstr "支持标题:" - -#: bookwyrm/templates/settings/site.html:69 -msgid "Admin email:" -msgstr "管理员邮件:" - -#: bookwyrm/templates/settings/site.html:79 -msgid "Allow registration:" -msgstr "允许注册:" - -#: bookwyrm/templates/settings/site.html:83 -msgid "Allow invite requests:" -msgstr "允许请求邀请:" - -#: bookwyrm/templates/settings/site.html:87 -msgid "Registration closed text:" -msgstr "注册关闭文字:" - -#: bookwyrm/templates/settings/user_admin.html:7 -#, python-format -msgid "Users: %(server_name)s" -msgstr "用户: %(server_name)s" - -#: bookwyrm/templates/settings/user_admin.html:20 -msgid "Username" -msgstr "用户名" - -#: bookwyrm/templates/settings/user_admin.html:24 -msgid "Date Added" -msgstr "添加日期:" - -#: bookwyrm/templates/settings/user_admin.html:28 -msgid "Last Active" -msgstr "最后或缺" - -#: bookwyrm/templates/settings/user_admin.html:36 -msgid "Remote server" -msgstr "移除服务器" - -#: bookwyrm/templates/settings/user_admin.html:45 -msgid "Active" -msgstr "活跃" - -#: bookwyrm/templates/settings/user_admin.html:45 -msgid "Inactive" -msgstr "停用" - -#: bookwyrm/templates/settings/user_admin.html:50 -msgid "Not set" -msgstr "未设置" - -#: bookwyrm/templates/snippets/block_button.html:5 -msgid "Block" -msgstr "屏蔽" - -#: bookwyrm/templates/snippets/block_button.html:10 -msgid "Un-block" -msgstr "取消屏蔽" - -#: bookwyrm/templates/snippets/book_titleby.html:3 -#, python-format -msgid "%(title)s by " -msgstr "%(title)s 来自" - -#: bookwyrm/templates/snippets/boost_button.html:8 -#: bookwyrm/templates/snippets/boost_button.html:9 -#: bookwyrm/templates/snippets/status/status_body.html:51 -#: bookwyrm/templates/snippets/status/status_body.html:52 -msgid "Boost status" -msgstr "转发状态" - -#: bookwyrm/templates/snippets/boost_button.html:16 -#: bookwyrm/templates/snippets/boost_button.html:17 -msgid "Un-boost status" -msgstr "取消转发状态" - -#: bookwyrm/templates/snippets/content_warning_field.html:3 -msgid "Spoiler alert:" -msgstr "剧透警告:" - -#: bookwyrm/templates/snippets/content_warning_field.html:4 -msgid "Spoilers ahead!" -msgstr "前有剧透!" - -#: bookwyrm/templates/snippets/create_status.html:9 -msgid "Review" -msgstr "书评" - -#: bookwyrm/templates/snippets/create_status.html:15 -msgid "Quote" -msgstr "引用" - -#: bookwyrm/templates/snippets/create_status_form.html:18 -msgid "Comment:" -msgstr "评论:" - -#: bookwyrm/templates/snippets/create_status_form.html:20 -msgid "Quote:" -msgstr "引用:" - -#: bookwyrm/templates/snippets/create_status_form.html:22 -msgid "Review:" -msgstr "书评:" - -#: bookwyrm/templates/snippets/create_status_form.html:29 -#: bookwyrm/templates/user/shelf.html:81 -msgid "Rating" -msgstr "评价" - -#: bookwyrm/templates/snippets/create_status_form.html:31 -#: bookwyrm/templates/snippets/rate_action.html:14 -#: bookwyrm/templates/snippets/stars.html:3 -msgid "No rating" -msgstr "没有评价" - -#: bookwyrm/templates/snippets/create_status_form.html:64 -#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:16 -msgid "Progress:" -msgstr "进度:" - -#: bookwyrm/templates/snippets/create_status_form.html:71 -#: bookwyrm/templates/snippets/readthrough_form.html:22 -#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:30 -msgid "pages" -msgstr "页数" - -#: bookwyrm/templates/snippets/create_status_form.html:72 -#: bookwyrm/templates/snippets/readthrough_form.html:23 -#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:31 -msgid "percent" -msgstr "百分比" - -#: bookwyrm/templates/snippets/create_status_form.html:77 -#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:36 -#, python-format -msgid "of %(pages)s pages" -msgstr "全书 %(pages)s 页" - -#: bookwyrm/templates/snippets/create_status_form.html:89 -msgid "Include spoiler alert" -msgstr "加入剧透警告" - -#: bookwyrm/templates/snippets/create_status_form.html:95 -#: bookwyrm/templates/snippets/privacy-icons.html:15 -#: bookwyrm/templates/snippets/privacy-icons.html:16 -#: bookwyrm/templates/snippets/privacy_select.html:19 -msgid "Private" -msgstr "私密" - -#: bookwyrm/templates/snippets/create_status_form.html:102 -msgid "Post" -msgstr "发布" - -#: bookwyrm/templates/snippets/delete_readthrough_modal.html:4 -msgid "Delete these read dates?" -msgstr "删除这些阅读日期吗?" - -#: bookwyrm/templates/snippets/delete_readthrough_modal.html:7 -#, python-format -msgid "You are deleting this readthrough and its %(count)s associated progress updates." -msgstr "你正要删除这篇阅读经过以及与之相关的 %(count)s 次进度更新。" - -#: bookwyrm/templates/snippets/delete_readthrough_modal.html:15 -#: bookwyrm/templates/snippets/follow_request_buttons.html:13 -msgid "Delete" -msgstr "删除" - -#: bookwyrm/templates/snippets/fav_button.html:7 -#: bookwyrm/templates/snippets/fav_button.html:8 -#: bookwyrm/templates/snippets/status/status_body.html:55 -#: bookwyrm/templates/snippets/status/status_body.html:56 -msgid "Like status" -msgstr "喜欢状态" - -#: bookwyrm/templates/snippets/fav_button.html:15 -#: bookwyrm/templates/snippets/fav_button.html:16 -msgid "Un-like status" -msgstr "取消喜欢状态" - -#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:7 -msgid "Show filters" -msgstr "显示过滤器" - -#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:9 -msgid "Hide filters" -msgstr "隐藏过滤器" - -#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:22 -msgid "Apply filters" -msgstr "应用过滤器" - -#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:26 -msgid "Clear filters" -msgstr "清除过滤器" - -#: bookwyrm/templates/snippets/follow_button.html:12 -msgid "Follow" -msgstr "关注" - -#: bookwyrm/templates/snippets/follow_button.html:18 -msgid "Undo follow request" -msgstr "撤回关注请求" - -#: bookwyrm/templates/snippets/follow_button.html:20 -msgid "Unfollow" -msgstr "取消关注" - -#: bookwyrm/templates/snippets/follow_request_buttons.html:8 -msgid "Accept" -msgstr "接受" - -#: bookwyrm/templates/snippets/generated_status/goal.html:1 -#, python-format -msgid "set a goal to read %(counter)s book in %(year)s" -msgid_plural "set a goal to read %(counter)s books in %(year)s" -msgstr[0] "设定了在 %(year)s 内要读 %(counter)s 本书的目标" - -#: bookwyrm/templates/snippets/generated_status/rating.html:3 -#, python-format -msgid "Rated %(title)s: %(display_rating)s star" -msgid_plural "Rated %(title)s: %(display_rating)s stars" -msgstr[0] "为 %(title)s 打了分: %(display_rating)s 星" - -#: bookwyrm/templates/snippets/generated_status/review_pure_name.html:4 -#, python-format -msgid "Review of \"%(book_title)s\" (%(display_rating)s star): %(review_title)s" -msgid_plural "Review of \"%(book_title)s\" (%(display_rating)s stars): %(review_title)s" -msgstr[0] "\"%(book_title)s\" 的书评(%(display_rating)s 星): %(review_title)s" - -#: bookwyrm/templates/snippets/generated_status/review_pure_name.html:8 -#, python-format -msgid "Review of \"%(book_title)s\": %(review_title)s" -msgstr "\"%(book_title)s\" 的书评: %(review_title)s" - -#: bookwyrm/templates/snippets/goal_card.html:23 -#, python-format -msgid "You can set or change your reading goal any time from your profile page" -msgstr "你可以在任何时候从你的个人资料页面 中设置或改变你的阅读目标" - -#: bookwyrm/templates/snippets/goal_form.html:9 -msgid "Reading goal:" -msgstr "阅读目标:" - -#: bookwyrm/templates/snippets/goal_form.html:14 -msgid "books" -msgstr "本书" - -#: bookwyrm/templates/snippets/goal_form.html:19 -msgid "Goal privacy:" -msgstr "目标隐私:" - -#: bookwyrm/templates/snippets/goal_form.html:26 -#: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:37 -#: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:29 -#: bookwyrm/templates/snippets/shelve_button/want_to_read_modal.html:20 -msgid "Post to feed" -msgstr "发布到消息流中" - -#: bookwyrm/templates/snippets/goal_form.html:30 -msgid "Set goal" -msgstr "设置目标" - -#: bookwyrm/templates/snippets/goal_progress.html:5 -msgid "Success!" -msgstr "成功!" - -#: bookwyrm/templates/snippets/goal_progress.html:7 -#, python-format -msgid "%(percent)s%% complete!" -msgstr "完成了 %(percent)s%% !" - -#: bookwyrm/templates/snippets/goal_progress.html:10 -#, python-format -msgid "You've read %(read_count)s of %(goal_count)s books." -msgstr "你已经阅读了 %(goal_count)s 本书中的 %(read_count)s 本。" - -#: bookwyrm/templates/snippets/goal_progress.html:12 -#, python-format -msgid "%(username)s has read %(read_count)s of %(goal_count)s books." -msgstr "%(username)s 已经阅读了 %(goal_count)s 本书中的 %(read_count)s 本。" - -#: bookwyrm/templates/snippets/page_text.html:4 -#, python-format -msgid "page %(page)s of %(total_pages)s" -msgstr "%(total_pages)s 页中的第 %(page)s 页" - -#: bookwyrm/templates/snippets/page_text.html:6 -#, python-format -msgid "page %(page)s" -msgstr "第 %(page)s 页" - -#: bookwyrm/templates/snippets/pagination.html:5 -msgid "Previous" -msgstr "往前" - -#: bookwyrm/templates/snippets/pagination.html:9 -msgid "Next" -msgstr "往后" - -#: bookwyrm/templates/snippets/privacy-icons.html:3 -#: bookwyrm/templates/snippets/privacy-icons.html:4 -#: bookwyrm/templates/snippets/privacy_select.html:10 -msgid "Public" -msgstr "公开" - -#: bookwyrm/templates/snippets/privacy-icons.html:7 -#: bookwyrm/templates/snippets/privacy-icons.html:8 -#: bookwyrm/templates/snippets/privacy_select.html:13 -msgid "Unlisted" -msgstr "不公开" - -#: bookwyrm/templates/snippets/privacy-icons.html:12 -msgid "Followers-only" -msgstr "仅关注者" - -#: bookwyrm/templates/snippets/privacy_select.html:6 -msgid "Post privacy" -msgstr "发文隐私" - -#: bookwyrm/templates/snippets/privacy_select.html:16 -#: bookwyrm/templates/user/followers.html:13 -msgid "Followers" -msgstr "关注者" - -#: bookwyrm/templates/snippets/rate_action.html:4 -msgid "Leave a rating" -msgstr "留下评价" - -#: bookwyrm/templates/snippets/rate_action.html:29 -msgid "Rate" -msgstr "评价" - -#: bookwyrm/templates/snippets/readthrough.html:8 -msgid "Progress Updates:" -msgstr "进度更新:" - -#: bookwyrm/templates/snippets/readthrough.html:14 -msgid "finished" -msgstr "已完成" - -#: bookwyrm/templates/snippets/readthrough.html:25 -msgid "Show all updates" -msgstr "显示所有更新" - -#: bookwyrm/templates/snippets/readthrough.html:41 -msgid "Delete this progress update" -msgstr "删除此进度更新" - -#: bookwyrm/templates/snippets/readthrough.html:51 -msgid "started" -msgstr "已开始" - -#: bookwyrm/templates/snippets/readthrough.html:57 -#: bookwyrm/templates/snippets/readthrough.html:71 -msgid "Edit read dates" -msgstr "编辑阅读日期" - -#: bookwyrm/templates/snippets/readthrough.html:61 -msgid "Delete these read dates" -msgstr "删除这些阅读日期" - -#: bookwyrm/templates/snippets/readthrough_form.html:7 -#: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:19 -#: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:17 -msgid "Started reading" -msgstr "已开始阅读" - -#: bookwyrm/templates/snippets/readthrough_form.html:14 -msgid "Progress" -msgstr "进度" - -#: bookwyrm/templates/snippets/readthrough_form.html:30 -#: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:25 -msgid "Finished reading" -msgstr "已完成阅读" - -#: bookwyrm/templates/snippets/register_form.html:32 -msgid "Sign Up" -msgstr "注册" - -#: bookwyrm/templates/snippets/report_button.html:5 -msgid "Report" -msgstr "报告" - -#: bookwyrm/templates/snippets/rss_title.html:5 -#: bookwyrm/templates/snippets/status/status_header.html:11 -msgid "rated" -msgstr "评价了" - -#: bookwyrm/templates/snippets/rss_title.html:7 -#: bookwyrm/templates/snippets/status/status_header.html:13 -msgid "reviewed" -msgstr "写了书评给" - -#: bookwyrm/templates/snippets/rss_title.html:9 -#: bookwyrm/templates/snippets/status/status_header.html:15 -msgid "commented on" -msgstr "评论了" - -#: bookwyrm/templates/snippets/rss_title.html:11 -#: bookwyrm/templates/snippets/status/status_header.html:17 -msgid "quoted" -msgstr "引用了" - -#: bookwyrm/templates/snippets/search_result_text.html:10 -msgid "No cover" -msgstr "没有封面" - -#: bookwyrm/templates/snippets/search_result_text.html:22 -#, python-format -msgid "by %(author)s" -msgstr "由 %(author)s 所著" - -#: bookwyrm/templates/snippets/search_result_text.html:30 -msgid "Import book" -msgstr "导入书目" - -#: bookwyrm/templates/snippets/shelf_selector.html:4 -msgid "Move book" -msgstr "移动书目" - -#: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:5 -#, python-format -msgid "Finish \"%(book_title)s\"" -msgstr "完成 \"%(book_title)s\"" - -#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:5 -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:35 -msgid "Update progress" -msgstr "更新进度" - -#: bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown.html:5 -msgid "More shelves" -msgstr "更多书架" - -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:8 -msgid "Start reading" -msgstr "开始阅读" - -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:13 -msgid "Finish reading" -msgstr "完成阅读" - -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:16 -#: bookwyrm/templates/snippets/shelve_button/want_to_read_modal.html:26 -msgid "Want to read" -msgstr "想要阅读" - -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:48 -#, python-format -msgid "Remove from %(name)s" -msgstr "从 %(name)s 移除" - -#: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:5 -#, python-format -msgid "Start \"%(book_title)s\"" -msgstr "开始 \"%(book_title)s\"" - -#: bookwyrm/templates/snippets/shelve_button/want_to_read_modal.html:5 -#, python-format -msgid "Want to Read \"%(book_title)s\"" -msgstr "想要阅读 \"%(book_title)s\"" - -#: bookwyrm/templates/snippets/status/status.html:9 -msgid "boosted" -msgstr "转发了" - -#: bookwyrm/templates/snippets/status/status_body.html:27 -#: bookwyrm/templates/snippets/status/status_options.html:18 -msgid "Delete status" -msgstr "删除发文" - -#: bookwyrm/templates/snippets/status/status_body.html:34 -#: bookwyrm/templates/snippets/status/status_body.html:47 -#: bookwyrm/templates/snippets/status/status_body.html:48 -msgid "Reply" -msgstr "回复" - -#: bookwyrm/templates/snippets/status/status_content.html:18 -#: bookwyrm/templates/snippets/trimmed_text.html:15 -msgid "Show more" -msgstr "显示更多" - -#: bookwyrm/templates/snippets/status/status_content.html:25 -#: bookwyrm/templates/snippets/trimmed_text.html:25 -msgid "Show less" -msgstr "显示更少" - -#: bookwyrm/templates/snippets/status/status_content.html:46 -msgid "Open image in new window" -msgstr "在新窗口中打开图像" - -#: bookwyrm/templates/snippets/status/status_header.html:22 -#, python-format -msgid "replied to %(username)s's review" -msgstr "回复了 %(username)s书评" - -#: bookwyrm/templates/snippets/status/status_header.html:24 -#, python-format -msgid "replied to %(username)s's comment" -msgstr "恢复了 %(username)s评论" - -#: bookwyrm/templates/snippets/status/status_header.html:26 -#, python-format -msgid "replied to %(username)s's quote" -msgstr "回复了 %(username)s引用" - -#: bookwyrm/templates/snippets/status/status_header.html:28 -#, python-format -msgid "replied to %(username)s's status" -msgstr "回复了 %(username)s状态" - -#: bookwyrm/templates/snippets/status/status_options.html:7 -#: bookwyrm/templates/snippets/user_options.html:7 -msgid "More options" -msgstr "更多选项" - -#: bookwyrm/templates/snippets/switch_edition_button.html:5 -msgid "Switch to this edition" -msgstr "切换到此版本" - -#: bookwyrm/templates/snippets/table-sort-header.html:6 -msgid "Sorted ascending" -msgstr "升序排序" - -#: bookwyrm/templates/snippets/table-sort-header.html:10 -msgid "Sorted descending" -msgstr "降序排序" - -#: bookwyrm/templates/snippets/tag.html:14 -msgid "Remove tag" -msgstr "移除标签" - -#: bookwyrm/templates/snippets/tag.html:18 -msgid "Add tag" -msgstr "添加标签" - -#: bookwyrm/templates/tag.html:9 -#, python-format -msgid "Books tagged \"%(tag.name)s\"" -msgstr "标有 \"%(tag.name)s\" 标签的书" - -#: bookwyrm/templates/user/books_header.html:5 -#, python-format -msgid "%(username)s's books" -msgstr "%(username)s 的书目" - -#: bookwyrm/templates/user/create_shelf_form.html:5 -#: bookwyrm/templates/user/create_shelf_form.html:22 -msgid "Create Shelf" -msgstr "创建书架" - -#: bookwyrm/templates/user/edit_shelf_form.html:5 -msgid "Edit Shelf" -msgstr "编辑书架" - -#: bookwyrm/templates/user/edit_shelf_form.html:26 -msgid "Update shelf" -msgstr "更新书架" - -#: bookwyrm/templates/user/followers.html:7 -#: bookwyrm/templates/user/following.html:7 -#: bookwyrm/templates/user/user.html:10 -msgid "User Profile" -msgstr "用户个人资料" - -#: bookwyrm/templates/user/followers.html:29 -#, python-format -msgid "%(username)s has no followers" -msgstr "%(username)s 没有关注者" - -#: bookwyrm/templates/user/following.html:13 -msgid "Following" -msgstr "正在关注" - -#: bookwyrm/templates/user/following.html:29 -#, python-format -msgid "%(username)s isn't following any users" -msgstr "%(username)s 没有关注任何用户" - -#: bookwyrm/templates/user/lists.html:9 -msgid "Your Lists" -msgstr "你的列表" - -#: bookwyrm/templates/user/lists.html:11 -#, python-format -msgid "Lists: %(username)s" -msgstr "列表: %(username)s" - -#: bookwyrm/templates/user/lists.html:17 bookwyrm/templates/user/lists.html:29 -msgid "Create list" -msgstr "创建列表" - -#: bookwyrm/templates/user/shelf.html:24 bookwyrm/views/shelf.py:56 -msgid "All books" -msgstr "所有书目" - -#: bookwyrm/templates/user/shelf.html:37 -msgid "Create shelf" -msgstr "创建书架" - -#: bookwyrm/templates/user/shelf.html:58 -msgid "Edit shelf" -msgstr "编辑书架" - -#: bookwyrm/templates/user/shelf.html:78 -msgid "Shelved" -msgstr "上架时间" - -#: bookwyrm/templates/user/shelf.html:79 -msgid "Started" -msgstr "开始时间" - -#: bookwyrm/templates/user/shelf.html:80 -msgid "Finished" -msgstr "完成时间" - -#: bookwyrm/templates/user/shelf.html:129 -msgid "This shelf is empty." -msgstr "此书架是空的。" - -#: bookwyrm/templates/user/shelf.html:135 -msgid "Delete shelf" -msgstr "删除书架" - -#: bookwyrm/templates/user/user.html:16 -msgid "Edit profile" -msgstr "编辑个人资料" - -#: bookwyrm/templates/user/user.html:34 -#, python-format -msgid "View all %(size)s" -msgstr "查看所有 %(size)s 本" - -#: bookwyrm/templates/user/user.html:47 -msgid "View all books" -msgstr "查看所有书目" - -#: bookwyrm/templates/user/user.html:59 -#, python-format -msgid "Set a reading goal for %(year)s" -msgstr "设定 %(year)s 的阅读目标" - -#: bookwyrm/templates/user/user.html:65 -msgid "User Activity" -msgstr "用户活动" - -#: bookwyrm/templates/user/user.html:68 -msgid "RSS feed" -msgstr "RSS 流" - -#: bookwyrm/templates/user/user.html:79 -msgid "No activities yet!" -msgstr "还没有活动!" - -#: bookwyrm/templates/user/user_layout.html:32 -msgid "Follow Requests" -msgstr "关注请求" - -#: bookwyrm/templates/user/user_layout.html:56 -msgid "Reading Goal" -msgstr "阅读目标" - -#: bookwyrm/templates/user/user_layout.html:68 -msgid "Books" -msgstr "书目" - -#: bookwyrm/templates/user/user_preview.html:13 -#, python-format -msgid "Joined %(date)s" -msgstr "在 %(date)s 加入" - -#: bookwyrm/templates/user/user_preview.html:15 -#, python-format -msgid "%(counter)s follower" -msgid_plural "%(counter)s followers" -msgstr[0] "%(counter)s 个关注者" - -#: bookwyrm/templates/user/user_preview.html:16 -#, python-format -msgid "%(counter)s following" -msgstr "关注着 %(counter)s 人" - -#: bookwyrm/views/password.py:32 -msgid "No user with that email address was found." -msgstr "没有找到使用该邮箱的用户。" - -#: bookwyrm/views/password.py:41 -#, python-format -msgid "A password reset link sent to %s" -msgstr "密码重置连接已发送给 %s" - -#, fuzzy -#~| msgid "Started" -#~ msgid "Getting Started" -#~ msgstr "已开始" - -#, fuzzy -#~| msgid "No users found for \"%(query)s\"" -#~ msgid "No users were found for \"%(query)s\"" -#~ msgstr "没有找到 \"%(query)s\" 的用户" - -#~ msgid "Tags" -#~ msgstr "标签" - -#~ msgid "Your shelves" -#~ msgstr "你的书架" - -#~ msgid "Your lists" -#~ msgstr "你的列表" - -#~ msgid "See all %(size)s lists" -#~ msgstr "查看所有 %(size)s 个列表" - -#~ msgid "Recent Lists" -#~ msgstr "最近的列表" - -#~ msgid "Published" -#~ msgstr "已出版" - -#~ msgid "External links" -#~ msgstr "外部链接" - -#~ msgid "OpenLibrary" -#~ msgstr "OpenLibrary" - -#~ msgid "Change shelf" -#~ msgstr "更换书架" - -#~ msgid "Unshelve" -#~ msgstr "取下书架" - -#~ msgid "Your Shelves" -#~ msgstr "你的书架" - -#~ msgid "%(username)s: Shelves" -#~ msgstr "%(username)s: 书架" - -#~ msgid "Shelves" -#~ msgstr "书架" - -#~ msgid "See all %(shelf_count)s shelves" -#~ msgstr "查看所有 %(shelf_count)s 个书架" - -#~ msgid "Send follow request" -#~ msgstr "发送关注请求" - -#, fuzzy -#~| msgid "All messages" -#~ msgid "Messages" -#~ msgstr "所有消息" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "Enter a valid email address." -#~ msgstr "邮箱地址:" - -#, fuzzy -#~| msgid "Series number:" -#~ msgid "Enter a number." -#~ msgstr "系列编号:" - -#, fuzzy -#~| msgid "Series number:" -#~ msgid "Decimal number" -#~ msgstr "系列编号:" - -#, fuzzy -#~| msgid "List curation:" -#~ msgid "Duration" -#~ msgstr "列表策展:" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "Email address" -#~ msgstr "邮箱地址:" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "IPv4 address" -#~ msgstr "邮箱地址:" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "IP address" -#~ msgstr "邮箱地址:" - -#, fuzzy -#~| msgid "No active invites" -#~ msgid "Positive integer" -#~ msgstr "无有效的邀请" - -#, fuzzy -#~| msgid "Images" -#~ msgid "Image" -#~ msgstr "图像" - -#, fuzzy -#~| msgid "Relationships" -#~ msgid "One-to-one relationship" -#~ msgstr "关系" - -#, fuzzy -#~| msgid "This shelf is empty." -#~ msgid "This field is required." -#~ msgstr "此书架是空的。" - -#, fuzzy -#~| msgid "This shelf is empty." -#~ msgid "The submitted file is empty." -#~ msgstr "此书架是空的。" - -#, fuzzy -#~| msgid "Start reading" -#~ msgid "Currently" -#~ msgstr "开始阅读" - -#, fuzzy -#~| msgid "Change shelf" -#~ msgid "Change" -#~ msgstr "改变书架" - -#, fuzzy -#~| msgid "Status" -#~ msgid "Sat" -#~ msgstr "状态" - -#, fuzzy -#~| msgid "Search" -#~ msgid "March" -#~ msgstr "搜索" - -#, fuzzy -#~| msgid "Series number:" -#~ msgid "September" -#~ msgstr "系列编号:" - -#, fuzzy -#~| msgid "Search" -#~ msgctxt "abbrev. month" -#~ msgid "March" -#~ msgstr "搜索" - -#, fuzzy -#~| msgid "Search" -#~ msgctxt "alt. month" -#~ msgid "March" -#~ msgstr "搜索" - -#, fuzzy -#~| msgid "Series number:" -#~ msgctxt "alt. month" -#~ msgid "September" -#~ msgstr "系列编号:" - -#, fuzzy -#~| msgid "No books found matching the query \"%(query)s\"" -#~ msgid "No %(verbose_name)s found matching the query" -#~ msgstr "没有符合 \"%(query)s\" 请求的书目" - -#~ msgid "Announcements" -#~ msgstr "公告" - -#~ msgid "Site Configuration" -#~ msgstr "站点配置" - -#~ msgid "Follow request already sent." -#~ msgstr "已经发送关注请求。" - -#~ msgid "Created and curated by" -#~ msgstr "创建者及策展者为" - -#~ msgid "Created by" -#~ msgstr "创建者为" - -#~ msgid "Create New Shelf" -#~ msgstr "新建书架" - -#~ msgid "Create new list" -#~ msgstr "新建列表" - -#~ msgid "Added by" -#~ msgstr "添加来自" diff --git a/locale/zh_CN/LC_MESSAGES/django.mo b/locale/zh_Hans/LC_MESSAGES/django.mo similarity index 100% rename from locale/zh_CN/LC_MESSAGES/django.mo rename to locale/zh_Hans/LC_MESSAGES/django.mo diff --git a/locale/zh_Hans/LC_MESSAGES/django.po b/locale/zh_Hans/LC_MESSAGES/django.po new file mode 100644 index 00000000..789960cc --- /dev/null +++ b/locale/zh_Hans/LC_MESSAGES/django.po @@ -0,0 +1,4412 @@ +# Simplified Chinese language text for the BookWyrm UI +# Copyright (C) 2021 Mouse Reeve +# This file is distributed under the same license as the bookwyrm package. +# Mouse Reeve , 2021. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 0.1.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-26 09:56-0700\n" +"PO-Revision-Date: 2021-03-20 00:56+0000\n" +"Last-Translator: Kana \n" +"Language-Team: Mouse Reeve \n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: bookwyrm/forms.py:224 +msgid "A user with this email already exists." +msgstr "已经存在使用该邮箱的用户。" + +#: bookwyrm/forms.py:238 +msgid "One Day" +msgstr "一天" + +#: bookwyrm/forms.py:239 +msgid "One Week" +msgstr "一周" + +#: bookwyrm/forms.py:240 +msgid "One Month" +msgstr "一个月" + +#: bookwyrm/forms.py:241 +msgid "Does Not Expire" +msgstr "永不失效" + +#: bookwyrm/forms.py:246 +#, python-format +msgid "%(count)d uses" +msgstr "%(count)d 次使用" + +#: bookwyrm/forms.py:249 +msgid "Unlimited" +msgstr "不受限" + +#: bookwyrm/forms.py:293 +msgid "List Order" +msgstr "" + +#: bookwyrm/forms.py:294 +#, fuzzy +#| msgid "Title" +msgid "Book Title" +msgstr "标题" + +#: bookwyrm/forms.py:295 bookwyrm/templates/snippets/create_status_form.html:31 +#: bookwyrm/templates/user/shelf.html:80 bookwyrm/templates/user/shelf.html:110 +msgid "Rating" +msgstr "评价" + +#: bookwyrm/forms.py:297 bookwyrm/templates/lists/list.html:82 +msgid "Sort By" +msgstr "" + +#: bookwyrm/forms.py:301 +#, fuzzy +#| msgid "Sorted ascending" +msgid "Ascending" +msgstr "升序排序" + +#: bookwyrm/forms.py:302 +#, fuzzy +#| msgid "Sorted ascending" +msgid "Descending" +msgstr "升序排序" + +#: bookwyrm/models/fields.py:24 +#, python-format +msgid "%(value)s is not a valid remote_id" +msgstr "%(value)s 不是有效的 remote_id" + +#: bookwyrm/models/fields.py:33 bookwyrm/models/fields.py:42 +#, python-format +msgid "%(value)s is not a valid username" +msgstr "%(value)s 不是有效的用户名" + +#: bookwyrm/models/fields.py:165 bookwyrm/templates/layout.html:153 +msgid "username" +msgstr "用户名" + +#: bookwyrm/models/fields.py:170 +msgid "A user with that username already exists." +msgstr "已经存在使用该用户名的用户。" + +#: bookwyrm/settings.py:152 +msgid "English" +msgstr "English(英语)" + +#: bookwyrm/settings.py:153 +msgid "German" +msgstr "Deutsch(德语)" + +#: bookwyrm/settings.py:154 +msgid "Spanish" +msgstr "Español(西班牙语)" + +#: bookwyrm/settings.py:155 +msgid "French" +msgstr "Français(法语)" + +#: bookwyrm/settings.py:156 +msgid "Simplified Chinese" +msgstr "简体中文" + +#: bookwyrm/templates/404.html:4 bookwyrm/templates/404.html:8 +msgid "Not Found" +msgstr "未找到" + +#: bookwyrm/templates/404.html:9 +msgid "The page you requested doesn't seem to exist!" +msgstr "你请求的页面似乎并不存在!" + +#: bookwyrm/templates/500.html:4 +msgid "Oops!" +msgstr "哎呀!" + +#: bookwyrm/templates/500.html:8 +msgid "Server Error" +msgstr "服务器错误" + +#: bookwyrm/templates/500.html:9 +msgid "Something went wrong! Sorry about that." +msgstr "某些东西出错了!对不起啦。" + +#: bookwyrm/templates/author.html:16 bookwyrm/templates/author.html:17 +msgid "Edit Author" +msgstr "编辑作者" + +#: bookwyrm/templates/author.html:32 +msgid "Wikipedia" +msgstr "维基百科" + +#: bookwyrm/templates/author.html:37 +#, python-format +msgid "Books by %(name)s" +msgstr "%(name)s 所著的书" + +#: bookwyrm/templates/book/book.html:33 +#: bookwyrm/templates/discover/large-book.html:12 +#: bookwyrm/templates/discover/small-book.html:9 +msgid "by" +msgstr "作者" + +#: bookwyrm/templates/book/book.html:41 bookwyrm/templates/book/book.html:42 +msgid "Edit Book" +msgstr "编辑书目" + +#: bookwyrm/templates/book/book.html:61 +#: bookwyrm/templates/book/cover_modal.html:5 +msgid "Add cover" +msgstr "添加封面" + +#: bookwyrm/templates/book/book.html:65 +msgid "Failed to load cover" +msgstr "加载封面失败" + +#: bookwyrm/templates/book/book.html:82 +msgid "View on OpenLibrary" +msgstr "在 OpenLibrary 查看" + +#: bookwyrm/templates/book/book.html:102 +#, python-format +msgid "(%(review_count)s review)" +msgid_plural "(%(review_count)s reviews)" +msgstr[0] "(%(review_count)s 则书评)" + +#: bookwyrm/templates/book/book.html:114 +msgid "Add Description" +msgstr "添加描述" + +#: bookwyrm/templates/book/book.html:121 +#: bookwyrm/templates/book/edit_book.html:107 +#: bookwyrm/templates/lists/form.html:12 +msgid "Description:" +msgstr "描述:" + +#: bookwyrm/templates/book/book.html:125 +#: bookwyrm/templates/book/edit_book.html:240 +#: bookwyrm/templates/edit_author.html:78 bookwyrm/templates/lists/form.html:42 +#: bookwyrm/templates/preferences/edit_user.html:70 +#: bookwyrm/templates/settings/edit_server.html:68 +#: bookwyrm/templates/settings/federated_server.html:93 +#: bookwyrm/templates/settings/site.html:93 +#: bookwyrm/templates/snippets/readthrough.html:75 +#: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:42 +#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:42 +#: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:34 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:38 +msgid "Save" +msgstr "保存" + +#: bookwyrm/templates/book/book.html:126 bookwyrm/templates/book/book.html:175 +#: bookwyrm/templates/book/cover_modal.html:32 +#: bookwyrm/templates/book/edit_book.html:241 +#: bookwyrm/templates/edit_author.html:79 +#: bookwyrm/templates/moderation/report_modal.html:34 +#: bookwyrm/templates/settings/federated_server.html:94 +#: bookwyrm/templates/snippets/delete_readthrough_modal.html:17 +#: bookwyrm/templates/snippets/goal_form.html:32 +#: bookwyrm/templates/snippets/readthrough.html:76 +#: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:43 +#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:43 +#: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:35 +#: bookwyrm/templates/snippets/shelve_button/want_to_read_modal.html:28 +msgid "Cancel" +msgstr "取消" + +#: bookwyrm/templates/book/book.html:135 +#, python-format +msgid "%(count)s editions" +msgstr "%(count)s 个版本" + +#: bookwyrm/templates/book/book.html:143 +#, python-format +msgid "This edition is on your %(shelf_name)s shelf." +msgstr "此版本在你的 %(shelf_name)s 书架上。" + +#: bookwyrm/templates/book/book.html:149 +#, python-format +msgid "" +"A different edition of this book is on your %(shelf_name)s shelf." +msgstr "" +"本书的 另一个版本 在你的 %(shelf_name)s 书架上。" + +#: bookwyrm/templates/book/book.html:158 +msgid "Your reading activity" +msgstr "你的阅读活动" + +#: bookwyrm/templates/book/book.html:160 +msgid "Add read dates" +msgstr "添加阅读日期" + +#: bookwyrm/templates/book/book.html:165 +msgid "You don't have any reading activity for this book." +msgstr "你还没有任何这本书的阅读活动。" + +#: bookwyrm/templates/book/book.html:172 +msgid "Create" +msgstr "创建" + +#: bookwyrm/templates/book/book.html:194 +msgid "Subjects" +msgstr "主题" + +#: bookwyrm/templates/book/book.html:206 +msgid "Places" +msgstr "地点" + +#: bookwyrm/templates/book/book.html:217 bookwyrm/templates/layout.html:65 +#: bookwyrm/templates/lists/lists.html:5 bookwyrm/templates/lists/lists.html:12 +#: bookwyrm/templates/search_results.html:91 +#: bookwyrm/templates/user/user_layout.html:62 +msgid "Lists" +msgstr "列表" + +#: bookwyrm/templates/book/book.html:228 +msgid "Add to list" +msgstr "添加到列表" + +#: bookwyrm/templates/book/book.html:238 +#: bookwyrm/templates/book/cover_modal.html:31 +#: bookwyrm/templates/lists/list.html:133 +msgid "Add" +msgstr "添加" + +#: bookwyrm/templates/book/book.html:254 +#, fuzzy +#| msgid "Review" +msgid "Reviews" +msgstr "书评" + +#: bookwyrm/templates/book/book.html:259 +#, fuzzy +#| msgid "Your shelves" +msgid "Your reviews" +msgstr "你的书架" + +#: bookwyrm/templates/book/book.html:265 +#, fuzzy +#| msgid "Your Account" +msgid "Your comments" +msgstr "你的帐号" + +#: bookwyrm/templates/book/book.html:271 +#, fuzzy +#| msgid "Your books" +msgid "Your quotes" +msgstr "你的书目" + +#: bookwyrm/templates/book/book.html:305 +msgid "rated it" +msgstr "评价了" + +#: bookwyrm/templates/book/book_identifiers.html:8 +msgid "ISBN:" +msgstr "ISBN:" + +#: bookwyrm/templates/book/book_identifiers.html:15 +#: bookwyrm/templates/book/edit_book.html:226 +msgid "OCLC Number:" +msgstr "OCLC 号:" + +#: bookwyrm/templates/book/book_identifiers.html:22 +#: bookwyrm/templates/book/edit_book.html:230 +msgid "ASIN:" +msgstr "ASIN:" + +#: bookwyrm/templates/book/cover_modal.html:17 +#: bookwyrm/templates/book/edit_book.html:178 +msgid "Upload cover:" +msgstr "上传封面:" + +#: bookwyrm/templates/book/cover_modal.html:23 +#: bookwyrm/templates/book/edit_book.html:184 +msgid "Load cover from url:" +msgstr "从网址加载封面:" + +#: bookwyrm/templates/book/edit_book.html:5 +#: bookwyrm/templates/book/edit_book.html:11 +#, python-format +msgid "Edit \"%(book_title)s\"" +msgstr "编辑 \"%(book_title)s\"" + +#: bookwyrm/templates/book/edit_book.html:5 +#: bookwyrm/templates/book/edit_book.html:13 +msgid "Add Book" +msgstr "添加书目" + +#: bookwyrm/templates/book/edit_book.html:18 +#: bookwyrm/templates/edit_author.html:13 +msgid "Added:" +msgstr "添加了:" + +#: bookwyrm/templates/book/edit_book.html:19 +#: bookwyrm/templates/edit_author.html:14 +msgid "Updated:" +msgstr "更新了:" + +#: bookwyrm/templates/book/edit_book.html:20 +#: bookwyrm/templates/edit_author.html:15 +msgid "Last edited by:" +msgstr "最后编辑人:" + +#: bookwyrm/templates/book/edit_book.html:40 +msgid "Confirm Book Info" +msgstr "确认书目信息" + +#: bookwyrm/templates/book/edit_book.html:47 +#, python-format +msgid "Is \"%(name)s\" an existing author?" +msgstr "\"%(name)s\" 是已存在的作者吗?" + +#: bookwyrm/templates/book/edit_book.html:52 +#, python-format +msgid "Author of %(book_title)s" +msgstr "%(book_title)s 的作者" + +#: bookwyrm/templates/book/edit_book.html:55 +msgid "This is a new author" +msgstr "这是一位新的作者" + +#: bookwyrm/templates/book/edit_book.html:61 +#, python-format +msgid "Creating a new author: %(name)s" +msgstr "正在创建新的作者: %(name)s" + +#: bookwyrm/templates/book/edit_book.html:67 +msgid "Is this an edition of an existing work?" +msgstr "这是已存在的作品的一个版本吗?" + +#: bookwyrm/templates/book/edit_book.html:71 +msgid "This is a new work" +msgstr "这是一个新的作品。" + +#: bookwyrm/templates/book/edit_book.html:77 +#: bookwyrm/templates/password_reset.html:30 +msgid "Confirm" +msgstr "确认" + +#: bookwyrm/templates/book/edit_book.html:79 +#: bookwyrm/templates/feed/status.html:8 +msgid "Back" +msgstr "返回" + +#: bookwyrm/templates/book/edit_book.html:90 +#: bookwyrm/templates/edit_author.html:31 +msgid "Metadata" +msgstr "元数据" + +#: bookwyrm/templates/book/edit_book.html:92 +msgid "Title:" +msgstr "标题:" + +#: bookwyrm/templates/book/edit_book.html:100 +msgid "Subtitle:" +msgstr "副标题:" + +#: bookwyrm/templates/book/edit_book.html:113 +msgid "Series:" +msgstr "系列:" + +#: bookwyrm/templates/book/edit_book.html:120 +msgid "Series number:" +msgstr "系列编号:" + +#: bookwyrm/templates/book/edit_book.html:126 +msgid "Publisher:" +msgstr "出版社:" + +#: bookwyrm/templates/book/edit_book.html:128 +msgid "Separate multiple publishers with commas." +msgstr "请用英文逗号(,)分开多个出版社。" + +#: bookwyrm/templates/book/edit_book.html:135 +msgid "First published date:" +msgstr "初版时间:" + +#: bookwyrm/templates/book/edit_book.html:143 +msgid "Published date:" +msgstr "出版时间:" + +#: bookwyrm/templates/book/edit_book.html:152 +msgid "Authors" +msgstr "作者" + +#: bookwyrm/templates/book/edit_book.html:158 +#, python-format +msgid "Remove %(name)s" +msgstr "移除 %(name)s" + +#: bookwyrm/templates/book/edit_book.html:163 +msgid "Add Authors:" +msgstr "添加作者:" + +#: bookwyrm/templates/book/edit_book.html:164 +msgid "John Doe, Jane Smith" +msgstr "张三, 李四" + +#: bookwyrm/templates/book/edit_book.html:170 +#: bookwyrm/templates/user/shelf.html:74 +msgid "Cover" +msgstr "封面" + +#: bookwyrm/templates/book/edit_book.html:197 +msgid "Physical Properties" +msgstr "实体性质" + +#: bookwyrm/templates/book/edit_book.html:198 +#: bookwyrm/templates/book/format_filter.html:5 +msgid "Format:" +msgstr "格式:" + +#: bookwyrm/templates/book/edit_book.html:206 +msgid "Pages:" +msgstr "页数:" + +#: bookwyrm/templates/book/edit_book.html:213 +msgid "Book Identifiers" +msgstr "书目标识号" + +#: bookwyrm/templates/book/edit_book.html:214 +msgid "ISBN 13:" +msgstr "ISBN 13:" + +#: bookwyrm/templates/book/edit_book.html:218 +msgid "ISBN 10:" +msgstr "ISBN 10:" + +#: bookwyrm/templates/book/edit_book.html:222 +#: bookwyrm/templates/edit_author.html:59 +msgid "Openlibrary key:" +msgstr "Openlibrary key:" + +#: bookwyrm/templates/book/editions.html:5 +#, python-format +msgid "Editions of %(book_title)s" +msgstr "%(book_title)s 的各版本" + +#: bookwyrm/templates/book/editions.html:9 +#, python-format +msgid "Editions of \"%(work_title)s\"" +msgstr "\"%(work_title)s\" 的各版本" + +#: bookwyrm/templates/book/format_filter.html:8 +#: bookwyrm/templates/book/language_filter.html:8 +msgid "Any" +msgstr "所有" + +#: bookwyrm/templates/book/language_filter.html:5 +msgid "Language:" +msgstr "语言:" + +#: bookwyrm/templates/book/publisher_info.html:22 +#, python-format +msgid "%(format)s" +msgstr "" + +#: bookwyrm/templates/book/publisher_info.html:24 +#, python-format +msgid "%(format)s, %(pages)s pages" +msgstr "%(format)s, %(pages)s 页" + +#: bookwyrm/templates/book/publisher_info.html:26 +#, python-format +msgid "%(pages)s pages" +msgstr "%(pages)s 页" + +#: bookwyrm/templates/book/publisher_info.html:38 +#, python-format +msgid "%(languages)s language" +msgstr "%(languages)s 语言" + +#: bookwyrm/templates/book/publisher_info.html:64 +#, python-format +msgid "Published %(date)s by %(publisher)s." +msgstr "在 %(date)s 由 %(publisher)s 出版。" + +#: bookwyrm/templates/book/publisher_info.html:66 +#, python-format +msgid "Published %(date)s" +msgstr "于 %(date)s 出版" + +#: bookwyrm/templates/book/publisher_info.html:68 +#, python-format +msgid "Published by %(publisher)s." +msgstr "由 %(publisher)s 出版。" + +#: bookwyrm/templates/components/inline_form.html:8 +#: bookwyrm/templates/components/modal.html:11 +#: bookwyrm/templates/feed/feed_layout.html:70 +#: bookwyrm/templates/get_started/layout.html:19 +#: bookwyrm/templates/get_started/layout.html:52 +msgid "Close" +msgstr "关闭" + +#: bookwyrm/templates/compose.html:5 bookwyrm/templates/compose.html:8 +#, fuzzy +#| msgid "Boost status" +msgid "Compose status" +msgstr "转发状态" + +#: bookwyrm/templates/directory/community_filter.html:5 +msgid "Community" +msgstr "社区" + +#: bookwyrm/templates/directory/community_filter.html:8 +msgid "Local users" +msgstr "本地用户" + +#: bookwyrm/templates/directory/community_filter.html:12 +msgid "Federated community" +msgstr "跨站社区" + +#: bookwyrm/templates/directory/directory.html:6 +#: bookwyrm/templates/directory/directory.html:11 +#: bookwyrm/templates/layout.html:93 +msgid "Directory" +msgstr "目录" + +#: bookwyrm/templates/directory/directory.html:19 +msgid "Make your profile discoverable to other BookWyrm users." +msgstr "使你的个人资料可以被其它 BookWyrm 用户发现。" + +#: bookwyrm/templates/directory/directory.html:26 +#, python-format +msgid "" +"You can opt-out at any time in your profile settings." +msgstr "你可以在任何时候从你的 个人资料设定 中退出。" + +#: bookwyrm/templates/directory/directory.html:31 +#: bookwyrm/templates/snippets/goal_card.html:22 +msgid "Dismiss message" +msgstr "遣散消息" + +#: bookwyrm/templates/directory/directory.html:71 +msgid "follower you follow" +msgid_plural "followers you follow" +msgstr[0] "你关注的关注者" + +#: bookwyrm/templates/directory/directory.html:78 +msgid "book on your shelves" +msgid_plural "books on your shelves" +msgstr[0] "你书架上的书" + +#: bookwyrm/templates/directory/directory.html:86 +msgid "posts" +msgstr "发文" + +#: bookwyrm/templates/directory/directory.html:92 +msgid "last active" +msgstr "最后活跃" + +#: bookwyrm/templates/directory/sort_filter.html:5 +msgid "Order by" +msgstr "排列顺序" + +#: bookwyrm/templates/directory/sort_filter.html:8 +msgid "Suggested" +msgstr "受推荐" + +#: bookwyrm/templates/directory/sort_filter.html:9 +msgid "Recently active" +msgstr "最近活跃" + +#: bookwyrm/templates/directory/user_type_filter.html:5 +msgid "User type" +msgstr "用户类型" + +#: bookwyrm/templates/directory/user_type_filter.html:8 +msgid "BookWyrm users" +msgstr "BookWyrm 用户" + +#: bookwyrm/templates/directory/user_type_filter.html:12 +msgid "All known users" +msgstr "所有已知用户" + +#: bookwyrm/templates/discover/about.html:7 +#, python-format +msgid "About %(site_name)s" +msgstr "关于 %(site_name)s" + +#: bookwyrm/templates/discover/about.html:10 +#: bookwyrm/templates/discover/about.html:20 +msgid "Code of Conduct" +msgstr "行为准则" + +#: bookwyrm/templates/discover/about.html:13 +#: bookwyrm/templates/discover/about.html:29 +msgid "Privacy Policy" +msgstr "隐私政策" + +#: bookwyrm/templates/discover/discover.html:6 +msgid "Recent Books" +msgstr "最近书目" + +#: bookwyrm/templates/discover/landing_layout.html:5 +#: bookwyrm/templates/get_started/layout.html:4 +msgid "Welcome" +msgstr "欢迎" + +#: bookwyrm/templates/discover/landing_layout.html:17 +msgid "Decentralized" +msgstr "去中心化" + +#: bookwyrm/templates/discover/landing_layout.html:23 +msgid "Friendly" +msgstr "友好" + +#: bookwyrm/templates/discover/landing_layout.html:29 +msgid "Anti-Corporate" +msgstr "反企业" + +#: bookwyrm/templates/discover/landing_layout.html:44 +#, python-format +msgid "Join %(name)s" +msgstr "加入 %(name)s" + +#: bookwyrm/templates/discover/landing_layout.html:51 +#: bookwyrm/templates/login.html:48 +msgid "This instance is closed" +msgstr "本实例不开放。" + +#: bookwyrm/templates/discover/landing_layout.html:57 +msgid "Thank you! Your request has been received." +msgstr "谢谢你!我们已经受到了你的请求。" + +#: bookwyrm/templates/discover/landing_layout.html:60 +msgid "Request an Invitation" +msgstr "请求邀请" + +#: bookwyrm/templates/discover/landing_layout.html:64 +#: bookwyrm/templates/password_reset_request.html:18 +#: bookwyrm/templates/preferences/edit_user.html:38 +#: bookwyrm/templates/snippets/register_form.html:13 +msgid "Email address:" +msgstr "邮箱地址:" + +#: bookwyrm/templates/discover/landing_layout.html:70 +#: bookwyrm/templates/moderation/report_modal.html:33 +msgid "Submit" +msgstr "提交" + +#: bookwyrm/templates/discover/landing_layout.html:79 +msgid "Your Account" +msgstr "你的帐号" + +#: bookwyrm/templates/edit_author.html:5 +msgid "Edit Author:" +msgstr "编辑作者:" + +#: bookwyrm/templates/edit_author.html:32 bookwyrm/templates/lists/form.html:8 +#: bookwyrm/templates/user/create_shelf_form.html:13 +#: bookwyrm/templates/user/edit_shelf_form.html:14 +msgid "Name:" +msgstr "名称:" + +#: bookwyrm/templates/edit_author.html:37 +msgid "Bio:" +msgstr "简介:" + +#: bookwyrm/templates/edit_author.html:42 +msgid "Wikipedia link:" +msgstr "维基百科链接:" + +#: bookwyrm/templates/edit_author.html:47 +msgid "Birth date:" +msgstr "出生日期:" + +#: bookwyrm/templates/edit_author.html:52 +msgid "Death date:" +msgstr "死亡日期:" + +#: bookwyrm/templates/edit_author.html:58 +msgid "Author Identifiers" +msgstr "作者标识号:" + +#: bookwyrm/templates/edit_author.html:64 +msgid "Librarything key:" +msgstr "Librarything key:" + +#: bookwyrm/templates/edit_author.html:69 +msgid "Goodreads key:" +msgstr "Goodreads key:" + +#: bookwyrm/templates/email/html_layout.html:15 +#: bookwyrm/templates/email/text_layout.html:2 +msgid "Hi there," +msgstr "你好呀," + +#: bookwyrm/templates/email/html_layout.html:21 +#, python-format +msgid "" +"BookWyrm hosted on " +"%(site_name)s" +msgstr "" +"位于 %(site_name)s 的 BookWyrm" + +#: bookwyrm/templates/email/html_layout.html:23 +msgid "Email preference" +msgstr "邮箱偏好" + +#: bookwyrm/templates/email/invite/html_content.html:6 +#: bookwyrm/templates/email/invite/subject.html:2 +#, python-format +msgid "You're invited to join %(site_name)s!" +msgstr "你受邀请加入 %(site_name)s!" + +#: bookwyrm/templates/email/invite/html_content.html:9 +msgid "Join Now" +msgstr "立即加入" + +#: bookwyrm/templates/email/invite/html_content.html:15 +#, python-format +msgid "" +"Learn more about this instance." +msgstr "" +"了解更多 有关本实例的信息。" + +#: bookwyrm/templates/email/invite/text_content.html:4 +#, python-format +msgid "" +"You're invited to join %(site_name)s! Click the link below to create an " +"account." +msgstr "你受邀请加入 %(site_name)s!点击下面的连接来创建帐号。" + +#: bookwyrm/templates/email/invite/text_content.html:8 +msgid "Learn more about this instance:" +msgstr "了解更多有关本实例的信息:" + +#: bookwyrm/templates/email/password_reset/html_content.html:6 +#: bookwyrm/templates/email/password_reset/text_content.html:4 +#, python-format +msgid "" +"You requested to reset your %(site_name)s password. Click the link below to " +"set a new password and log in to your account." +msgstr "" +"你请求重置你在 %(site_name)s 的密码。点击下面的链接来设置新密码并登录你的帐" +"号。" + +#: bookwyrm/templates/email/password_reset/html_content.html:9 +#: bookwyrm/templates/password_reset.html:4 +#: bookwyrm/templates/password_reset.html:10 +#: bookwyrm/templates/password_reset_request.html:4 +#: bookwyrm/templates/password_reset_request.html:10 +msgid "Reset Password" +msgstr "重设密码" + +#: bookwyrm/templates/email/password_reset/html_content.html:13 +#: bookwyrm/templates/email/password_reset/text_content.html:8 +msgid "" +"If you didn't request to reset your password, you can ignore this email." +msgstr "如果你没有请求重设密码,你可以忽略这封邮件。" + +#: bookwyrm/templates/email/password_reset/subject.html:2 +#, python-format +msgid "Reset your %(site_name)s password" +msgstr "重置你在 %(site_name)s 的密码" + +#: bookwyrm/templates/feed/direct_messages.html:8 +#, python-format +msgid "Direct Messages with %(username)s" +msgstr "与 %(username)s 私信" + +#: bookwyrm/templates/feed/direct_messages.html:10 +#: bookwyrm/templates/layout.html:88 +msgid "Direct Messages" +msgstr "私信" + +#: bookwyrm/templates/feed/direct_messages.html:13 +msgid "All messages" +msgstr "所有消息" + +#: bookwyrm/templates/feed/direct_messages.html:22 +msgid "You have no messages right now." +msgstr "你现在没有消息。" + +#: bookwyrm/templates/feed/feed.html:9 +msgid "Home Timeline" +msgstr "主页时间线" + +#: bookwyrm/templates/feed/feed.html:11 +msgid "Local Timeline" +msgstr "本地时间线" + +#: bookwyrm/templates/feed/feed.html:13 +msgid "Federated Timeline" +msgstr "跨站时间线" + +#: bookwyrm/templates/feed/feed.html:19 +msgid "Home" +msgstr "主页" + +#: bookwyrm/templates/feed/feed.html:22 +msgid "Local" +msgstr "本站" + +#: bookwyrm/templates/feed/feed.html:25 +#: bookwyrm/templates/settings/edit_server.html:40 +msgid "Federated" +msgstr "跨站" + +#: bookwyrm/templates/feed/feed.html:33 +#, python-format +msgid "load 0 unread status(es)" +msgstr "加载 0 条未读状态" + +#: bookwyrm/templates/feed/feed.html:48 +msgid "" +"There aren't any activities right now! Try following a user to get started" +msgstr "现在还没有任何活动!尝试着从关注一个用户开始吧" + +#: bookwyrm/templates/feed/feed.html:56 +#: bookwyrm/templates/get_started/users.html:6 +msgid "Who to follow" +msgstr "可以关注的人" + +#: bookwyrm/templates/feed/feed_layout.html:5 +msgid "Updates" +msgstr "更新" + +#: bookwyrm/templates/feed/feed_layout.html:11 +#: bookwyrm/templates/layout.html:59 +#: bookwyrm/templates/user/books_header.html:3 +msgid "Your books" +msgstr "你的书目" + +#: bookwyrm/templates/feed/feed_layout.html:13 +msgid "" +"There are no books here right now! Try searching for a book to get started" +msgstr "现在这里还没有任何书目!尝试着从搜索某本书开始吧" + +#: bookwyrm/templates/feed/feed_layout.html:24 +#: bookwyrm/templates/user/shelf.html:28 +msgid "To Read" +msgstr "想读" + +#: bookwyrm/templates/feed/feed_layout.html:25 +#: bookwyrm/templates/user/shelf.html:28 +msgid "Currently Reading" +msgstr "在读" + +#: bookwyrm/templates/feed/feed_layout.html:26 +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:11 +#: bookwyrm/templates/user/shelf.html:28 +msgid "Read" +msgstr "读过" + +#: bookwyrm/templates/feed/feed_layout.html:88 bookwyrm/templates/goal.html:26 +#: bookwyrm/templates/snippets/goal_card.html:6 +#, python-format +msgid "%(year)s Reading Goal" +msgstr "%(year)s 阅读目标" + +#: bookwyrm/templates/feed/suggested_users.html:16 +#, python-format +msgid "%(mutuals)s follower you follow" +msgid_plural "%(mutuals)s followers you follow" +msgstr[0] "%(mutuals)s 个你也关注的关注者" + +#: bookwyrm/templates/feed/suggested_users.html:19 +#, python-format +msgid "%(shared_books)s book on your shelves" +msgid_plural "%(shared_books)s books on your shelves" +msgstr[0] "%(shared_books)s 本在你书架上也有的书" + +#: bookwyrm/templates/get_started/book_preview.html:6 +#, python-format +msgid "Have you read %(book_title)s?" +msgstr "你读过 %(book_title)s 了吗?" + +#: bookwyrm/templates/get_started/books.html:6 +msgid "What are you reading?" +msgstr "你在阅读什么?" + +#: bookwyrm/templates/get_started/books.html:9 +#: bookwyrm/templates/lists/list.html:101 +msgid "Search for a book" +msgstr "搜索书目" + +#: bookwyrm/templates/get_started/books.html:11 +#: bookwyrm/templates/isbn_search_results.html:17 +#: bookwyrm/templates/search_results.html:17 +#, python-format +msgid "No books found for \"%(query)s\"" +msgstr "没有找到 \"%(query)s\" 的书目" + +#: bookwyrm/templates/get_started/books.html:11 +#, python-format +msgid "You can add books when you start using %(site_name)s." +msgstr "你可以在开始使用 %(site_name)s 后添加书目。" + +#: bookwyrm/templates/get_started/books.html:16 +#: bookwyrm/templates/get_started/books.html:17 +#: bookwyrm/templates/get_started/users.html:18 +#: bookwyrm/templates/get_started/users.html:19 +#: bookwyrm/templates/layout.html:38 bookwyrm/templates/layout.html:39 +#: bookwyrm/templates/lists/list.html:105 +msgid "Search" +msgstr "搜索" + +#: bookwyrm/templates/get_started/books.html:26 +msgid "Suggested Books" +msgstr "推荐的书目" + +#: bookwyrm/templates/get_started/books.html:41 +#, python-format +msgid "Popular on %(site_name)s" +msgstr "%(site_name)s 上的热门" + +#: bookwyrm/templates/get_started/books.html:51 +#: bookwyrm/templates/lists/list.html:118 +msgid "No books found" +msgstr "没有找到书目" + +#: bookwyrm/templates/get_started/books.html:54 +#: bookwyrm/templates/get_started/profile.html:54 +msgid "Save & continue" +msgstr "保存 & 继续" + +#: bookwyrm/templates/get_started/layout.html:14 +#, python-format +msgid "Welcome to %(site_name)s!" +msgstr "欢迎来到 %(site_name)s!" + +#: bookwyrm/templates/get_started/layout.html:16 +msgid "These are some first steps to get you started." +msgstr "这些最初的步骤可以帮助你入门。" + +#: bookwyrm/templates/get_started/layout.html:30 +#: bookwyrm/templates/get_started/profile.html:6 +msgid "Create your profile" +msgstr "创建你的个人资料" + +#: bookwyrm/templates/get_started/layout.html:34 +msgid "Add books" +msgstr "添加书目" + +#: bookwyrm/templates/get_started/layout.html:38 +msgid "Find friends" +msgstr "寻找同好" + +#: bookwyrm/templates/get_started/layout.html:44 +msgid "Skip this step" +msgstr "跳过此步骤" + +#: bookwyrm/templates/get_started/layout.html:48 +msgid "Finish" +msgstr "完成" + +#: bookwyrm/templates/get_started/profile.html:15 +#: bookwyrm/templates/preferences/edit_user.html:24 +msgid "Display name:" +msgstr "显示名称:" + +#: bookwyrm/templates/get_started/profile.html:22 +#: bookwyrm/templates/preferences/edit_user.html:31 +msgid "Summary:" +msgstr "概要:" + +#: bookwyrm/templates/get_started/profile.html:23 +msgid "A little bit about you" +msgstr "少许关于你的信息" + +#: bookwyrm/templates/get_started/profile.html:32 +#: bookwyrm/templates/preferences/edit_user.html:17 +msgid "Avatar:" +msgstr "头像:" + +#: bookwyrm/templates/get_started/profile.html:42 +#: bookwyrm/templates/preferences/edit_user.html:52 +msgid "Manually approve followers:" +msgstr "手动批准关注者:" + +#: bookwyrm/templates/get_started/profile.html:48 +#: bookwyrm/templates/preferences/edit_user.html:58 +msgid "Show this account in suggested users:" +msgstr "在推荐的用户中显示此帐号:" + +#: bookwyrm/templates/get_started/profile.html:52 +msgid "" +"Your account will show up in the directory, and may be recommended to other " +"BookWyrm users." +msgstr "你的帐号会显示在目录中,并且可能会受其它 BookWyrm 用户推荐。" + +#: bookwyrm/templates/get_started/users.html:11 +msgid "Search for a user" +msgstr "搜索用户" + +#: bookwyrm/templates/get_started/users.html:13 +#: bookwyrm/templates/search_results.html:76 +#, python-format +msgid "No users found for \"%(query)s\"" +msgstr "没有找到 \"%(query)s\" 的用户" + +#: bookwyrm/templates/goal.html:7 +#, python-format +msgid "%(year)s Reading Progress" +msgstr "%(year)s 阅读进度" + +#: bookwyrm/templates/goal.html:11 +msgid "Edit Goal" +msgstr "编辑目标" + +#: bookwyrm/templates/goal.html:30 +#: bookwyrm/templates/snippets/goal_card.html:13 +#, python-format +msgid "" +"Set a goal for how many books you'll finish reading in %(year)s, and track " +"your progress throughout the year." +msgstr "设定一个 %(year)s 内要读多少书的目标,并记录你全年的进度。" + +#: bookwyrm/templates/goal.html:39 +#, python-format +msgid "%(name)s hasn't set a reading goal for %(year)s." +msgstr "%(name)s 还没有设定 %(year)s 的阅读目标。" + +#: bookwyrm/templates/goal.html:51 +#, python-format +msgid "Your %(year)s Books" +msgstr "你 %(year)s 的书目" + +#: bookwyrm/templates/goal.html:53 +#, python-format +msgid "%(username)s's %(year)s Books" +msgstr "%(username)s 在 %(year)s 的书目" + +#: bookwyrm/templates/import.html:5 bookwyrm/templates/import.html:9 +#: bookwyrm/templates/layout.html:98 +msgid "Import Books" +msgstr "导入书目" + +#: bookwyrm/templates/import.html:16 +msgid "Data source:" +msgstr "数据来源:" + +#: bookwyrm/templates/import.html:29 +msgid "Data file:" +msgstr "数据文件:" + +#: bookwyrm/templates/import.html:37 +msgid "Include reviews" +msgstr "纳入书评" + +#: bookwyrm/templates/import.html:42 +msgid "Privacy setting for imported reviews:" +msgstr "导入书评的隐私设定" + +#: bookwyrm/templates/import.html:48 +#: bookwyrm/templates/settings/server_blocklist.html:64 +msgid "Import" +msgstr "导入" + +#: bookwyrm/templates/import.html:53 +msgid "Recent Imports" +msgstr "最近的导入" + +#: bookwyrm/templates/import.html:55 +msgid "No recent imports" +msgstr "无最近的导入" + +#: bookwyrm/templates/import_status.html:6 +#: bookwyrm/templates/import_status.html:10 +msgid "Import Status" +msgstr "导入状态" + +#: bookwyrm/templates/import_status.html:13 +msgid "Import started:" +msgstr "导入开始:" + +#: bookwyrm/templates/import_status.html:17 +msgid "Import completed:" +msgstr "导入完成:" + +#: bookwyrm/templates/import_status.html:20 +msgid "TASK FAILED" +msgstr "任务失败" + +#: bookwyrm/templates/import_status.html:26 +msgid "Import still in progress." +msgstr "还在导入中。" + +#: bookwyrm/templates/import_status.html:28 +msgid "(Hit reload to update!)" +msgstr "(按下重新加载来更新!)" + +#: bookwyrm/templates/import_status.html:35 +msgid "Failed to load" +msgstr "加载失败" + +#: bookwyrm/templates/import_status.html:44 +#, python-format +msgid "" +"Jump to the bottom of the list to select the %(failed_count)s items which " +"failed to import." +msgstr "跳转至列表底部来选取 %(failed_count)s 个导入失败的项目。" + +#: bookwyrm/templates/import_status.html:79 +msgid "Select all" +msgstr "全选" + +#: bookwyrm/templates/import_status.html:82 +msgid "Retry items" +msgstr "重试项目" + +#: bookwyrm/templates/import_status.html:108 +msgid "Successfully imported" +msgstr "成功导入了" + +#: bookwyrm/templates/import_status.html:112 +#: bookwyrm/templates/lists/curate.html:14 +msgid "Book" +msgstr "书目" + +#: bookwyrm/templates/import_status.html:115 +#: bookwyrm/templates/snippets/create_status_form.html:10 +#: bookwyrm/templates/user/shelf.html:75 bookwyrm/templates/user/shelf.html:93 +msgid "Title" +msgstr "标题" + +#: bookwyrm/templates/import_status.html:118 +#: bookwyrm/templates/user/shelf.html:76 bookwyrm/templates/user/shelf.html:96 +msgid "Author" +msgstr "作者" + +#: bookwyrm/templates/import_status.html:141 +msgid "Imported" +msgstr "已导入" + +#: bookwyrm/templates/invite.html:4 bookwyrm/templates/invite.html:12 +#: bookwyrm/templates/login.html:43 +msgid "Create an Account" +msgstr "创建帐号" + +#: bookwyrm/templates/invite.html:21 +msgid "Permission Denied" +msgstr "没有权限" + +#: bookwyrm/templates/invite.html:22 +msgid "Sorry! This invite code is no longer valid." +msgstr "抱歉!此邀请码已不再有效。" + +#: bookwyrm/templates/isbn_search_results.html:4 +#: bookwyrm/templates/search_results.html:4 +msgid "Search Results" +msgstr "搜索结果" + +#: bookwyrm/templates/isbn_search_results.html:9 +#: bookwyrm/templates/search_results.html:9 +#, python-format +msgid "Search Results for \"%(query)s\"" +msgstr "\"%(query)s\" 的搜索结果" + +#: bookwyrm/templates/isbn_search_results.html:14 +#: bookwyrm/templates/search_results.html:14 +msgid "Matching Books" +msgstr "匹配的书目" + +#: bookwyrm/templates/layout.html:34 +msgid "Search for a book or user" +msgstr "搜索书目或用户" + +#: bookwyrm/templates/layout.html:48 bookwyrm/templates/layout.html:49 +msgid "Main navigation menu" +msgstr "主导航菜单" + +#: bookwyrm/templates/layout.html:62 +msgid "Feed" +msgstr "动态" + +#: bookwyrm/templates/layout.html:103 +msgid "Settings" +msgstr "设置" + +#: bookwyrm/templates/layout.html:112 +#: bookwyrm/templates/settings/admin_layout.html:31 +#: bookwyrm/templates/settings/manage_invite_requests.html:15 +#: bookwyrm/templates/settings/manage_invites.html:3 +#: bookwyrm/templates/settings/manage_invites.html:15 +msgid "Invites" +msgstr "邀请" + +#: bookwyrm/templates/layout.html:119 +msgid "Admin" +msgstr "管理员" + +#: bookwyrm/templates/layout.html:126 +msgid "Log out" +msgstr "登出" + +#: bookwyrm/templates/layout.html:134 bookwyrm/templates/layout.html:135 +#: bookwyrm/templates/notifications.html:6 +#: bookwyrm/templates/notifications.html:10 +msgid "Notifications" +msgstr "通知" + +#: bookwyrm/templates/layout.html:152 bookwyrm/templates/layout.html:156 +#: bookwyrm/templates/login.html:17 +#: bookwyrm/templates/snippets/register_form.html:4 +msgid "Username:" +msgstr "用户名:" + +#: bookwyrm/templates/layout.html:157 +msgid "password" +msgstr "密码" + +#: bookwyrm/templates/layout.html:158 bookwyrm/templates/login.html:36 +msgid "Forgot your password?" +msgstr "忘记了密码?" + +#: bookwyrm/templates/layout.html:161 bookwyrm/templates/login.html:10 +#: bookwyrm/templates/login.html:33 +msgid "Log in" +msgstr "登录" + +#: bookwyrm/templates/layout.html:169 +msgid "Join" +msgstr "加入" + +#: bookwyrm/templates/layout.html:195 +msgid "About this server" +msgstr "关于本服务器" + +#: bookwyrm/templates/layout.html:199 +msgid "Contact site admin" +msgstr "联系站点管理员" + +#: bookwyrm/templates/layout.html:206 +#, python-format +msgid "" +"Support %(site_name)s on " +"%(support_title)s" +msgstr "" +"在 %(support_title)s 上支" +"持 %(site_name)s" + +#: bookwyrm/templates/layout.html:210 +msgid "" +"BookWyrm is open source software. You can contribute or report issues on GitHub." +msgstr "" +"BookWyrm 是开源软件。你可以在 GitHub 贡献或报告问题。" + +#: bookwyrm/templates/lists/create_form.html:5 +#: bookwyrm/templates/lists/lists.html:20 +msgid "Create List" +msgstr "创建列表" + +#: bookwyrm/templates/lists/created_text.html:5 +#, python-format +msgid "Created and curated by %(username)s" +msgstr "由 %(username)s 创建并策展" + +#: bookwyrm/templates/lists/created_text.html:7 +#, python-format +msgid "Created by %(username)s" +msgstr "由 %(username)s 创建" + +#: bookwyrm/templates/lists/curate.html:6 +msgid "Pending Books" +msgstr "等候中的书目" + +#: bookwyrm/templates/lists/curate.html:7 +msgid "Go to list" +msgstr "前往列表" + +#: bookwyrm/templates/lists/curate.html:9 +msgid "You're all set!" +msgstr "都弄好了!" + +#: bookwyrm/templates/lists/curate.html:15 +msgid "Suggested by" +msgstr "推荐来自" + +#: bookwyrm/templates/lists/curate.html:35 +msgid "Approve" +msgstr "批准" + +#: bookwyrm/templates/lists/curate.html:41 +msgid "Discard" +msgstr "削除" + +#: bookwyrm/templates/lists/edit_form.html:5 +#: bookwyrm/templates/lists/list_layout.html:18 +msgid "Edit List" +msgstr "编辑列表" + +#: bookwyrm/templates/lists/form.html:18 +msgid "List curation:" +msgstr "列表策展:" + +#: bookwyrm/templates/lists/form.html:21 +msgid "Closed" +msgstr "已关闭" + +#: bookwyrm/templates/lists/form.html:22 +msgid "Only you can add and remove books to this list" +msgstr "只有你可以在此列表中添加或移除书目" + +#: bookwyrm/templates/lists/form.html:26 +msgid "Curated" +msgstr "策展" + +#: bookwyrm/templates/lists/form.html:27 +msgid "Anyone can suggest books, subject to your approval" +msgstr "任何人都可以推荐书目、主题让你批准" + +#: bookwyrm/templates/lists/form.html:31 +#: bookwyrm/templates/moderation/reports.html:25 +msgid "Open" +msgstr "开放" + +#: bookwyrm/templates/lists/form.html:32 +msgid "Anyone can add books to this list" +msgstr "任何人都可以向此列表中添加书目" + +#: bookwyrm/templates/lists/list.html:19 +msgid "You successfully suggested a book for this list!" +msgstr "" + +#: bookwyrm/templates/lists/list.html:21 +#, fuzzy +#| msgid "Anyone can add books to this list" +msgid "You successfully added a book to this list!" +msgstr "任何人都可以向此列表中添加书目" + +#: bookwyrm/templates/lists/list.html:27 +msgid "This list is currently empty" +msgstr "此列表当前是空的" + +#: bookwyrm/templates/lists/list.html:46 +#, python-format +msgid "Added by %(username)s" +msgstr "由 %(username)s 添加" + +#: bookwyrm/templates/lists/list.html:58 +#, fuzzy +#| msgid "Sent" +msgid "Set" +msgstr "已发送" + +#: bookwyrm/templates/lists/list.html:61 +#, fuzzy +#| msgid "List curation:" +msgid "List position" +msgstr "列表策展:" + +#: bookwyrm/templates/lists/list.html:67 +#: bookwyrm/templates/snippets/shelf_selector.html:26 +msgid "Remove" +msgstr "移除" + +#: bookwyrm/templates/lists/list.html:80 bookwyrm/templates/lists/list.html:92 +#, fuzzy +#| msgid "Your Lists" +msgid "Sort List" +msgstr "你的列表" + +#: bookwyrm/templates/lists/list.html:86 +#, fuzzy +#| msgid "Directory" +msgid "Direction" +msgstr "目录" + +#: bookwyrm/templates/lists/list.html:97 +msgid "Add Books" +msgstr "添加书目" + +#: bookwyrm/templates/lists/list.html:97 +msgid "Suggest Books" +msgstr "推荐书目" + +#: bookwyrm/templates/lists/list.html:106 +msgid "search" +msgstr "搜索" + +#: bookwyrm/templates/lists/list.html:112 +msgid "Clear search" +msgstr "清除搜索" + +#: bookwyrm/templates/lists/list.html:117 +#, python-format +msgid "No books found matching the query \"%(query)s\"" +msgstr "没有符合 \"%(query)s\" 请求的书目" + +#: bookwyrm/templates/lists/list.html:133 +msgid "Suggest" +msgstr "推荐" + +#: bookwyrm/templates/lists/lists.html:14 bookwyrm/templates/user/lists.html:9 +msgid "Your Lists" +msgstr "你的列表" + +#: bookwyrm/templates/login.html:4 +msgid "Login" +msgstr "登录" + +#: bookwyrm/templates/login.html:23 bookwyrm/templates/password_reset.html:17 +#: bookwyrm/templates/snippets/register_form.html:22 +msgid "Password:" +msgstr "密码:" + +#: bookwyrm/templates/login.html:49 +msgid "Contact an administrator to get an invite" +msgstr "联系管理员以取得邀请" + +#: bookwyrm/templates/login.html:59 +msgid "More about this site" +msgstr "关于本站点的更多" + +#: bookwyrm/templates/moderation/report.html:6 +#: bookwyrm/templates/moderation/report.html:7 +#: bookwyrm/templates/moderation/report_preview.html:6 +#, python-format +msgid "Report #%(report_id)s: %(username)s" +msgstr "报告 #%(report_id)s: %(username)s" + +#: bookwyrm/templates/moderation/report.html:11 +msgid "Back to reports" +msgstr "回到报告" + +#: bookwyrm/templates/moderation/report.html:23 +msgid "Moderator Comments" +msgstr "监察员评论" + +#: bookwyrm/templates/moderation/report.html:41 +#: bookwyrm/templates/snippets/create_status.html:28 +#: bookwyrm/templates/snippets/create_status_form.html:53 +msgid "Comment" +msgstr "评论" + +#: bookwyrm/templates/moderation/report.html:46 +msgid "Reported statuses" +msgstr "被报告的状态" + +#: bookwyrm/templates/moderation/report.html:48 +msgid "No statuses reported" +msgstr "没有被报告的状态" + +#: bookwyrm/templates/moderation/report.html:54 +#, fuzzy +#| msgid "Statuses has been deleted" +msgid "Status has been deleted" +msgstr "状态已被删除" + +#: bookwyrm/templates/moderation/report_modal.html:6 +#, python-format +msgid "Report @%(username)s" +msgstr "报告 %(username)s" + +#: bookwyrm/templates/moderation/report_modal.html:23 +#, python-format +msgid "This report will be sent to %(site_name)s's moderators for review." +msgstr "本报告会被发送至 %(site_name)s 的监察员以复查。" + +#: bookwyrm/templates/moderation/report_modal.html:24 +msgid "More info about this report:" +msgstr "关于本报告的更多信息" + +#: bookwyrm/templates/moderation/report_preview.html:13 +msgid "No notes provided" +msgstr "没有提供摘记" + +#: bookwyrm/templates/moderation/report_preview.html:20 +#, python-format +msgid "Reported by %(username)s" +msgstr "由 %(username)s 报告" + +#: bookwyrm/templates/moderation/report_preview.html:30 +msgid "Re-open" +msgstr "重新开启" + +#: bookwyrm/templates/moderation/report_preview.html:32 +msgid "Resolve" +msgstr "已解决" + +#: bookwyrm/templates/moderation/reports.html:6 +#, python-format +msgid "Reports: %(server_name)s" +msgstr "报告: %(server_name)s" + +#: bookwyrm/templates/moderation/reports.html:8 +#: bookwyrm/templates/moderation/reports.html:17 +#: bookwyrm/templates/settings/admin_layout.html:35 +msgid "Reports" +msgstr "报告" + +#: bookwyrm/templates/moderation/reports.html:14 +#, python-format +msgid "Reports: %(server_name)s" +msgstr "报告: %(server_name)s" + +#: bookwyrm/templates/moderation/reports.html:28 +msgid "Resolved" +msgstr "已解决" + +#: bookwyrm/templates/moderation/reports.html:37 +msgid "No reports found." +msgstr "没有找到报告" + +#: bookwyrm/templates/notifications.html:14 +msgid "Delete notifications" +msgstr "删除通知" + +#: bookwyrm/templates/notifications.html:53 +#, python-format +msgid "" +"favorited your review of %(book_title)s" +msgstr "" +"喜欢了你 %(book_title)s 的书评" + +#: bookwyrm/templates/notifications.html:55 +#, python-format +msgid "" +"favorited your comment on %(book_title)s" +msgstr "" +"喜欢了你 %(book_title)s 的评论" + +#: bookwyrm/templates/notifications.html:57 +#, python-format +msgid "" +"favorited your quote from %(book_title)s" +msgstr "" +"喜欢了你 来自 %(book_title)s 的引用" + +#: bookwyrm/templates/notifications.html:59 +#, python-format +msgid "favorited your status" +msgstr "喜欢了你的 状态" + +#: bookwyrm/templates/notifications.html:64 +#, python-format +msgid "" +"mentioned you in a review of " +"%(book_title)s" +msgstr "" +"在 %(book_title)s 的书评 里提到" +"了你" + +#: bookwyrm/templates/notifications.html:66 +#, python-format +msgid "" +"mentioned you in a comment on " +"%(book_title)s" +msgstr "" +"在 %(book_title)s 的评论 里提到" +"了你" + +#: bookwyrm/templates/notifications.html:68 +#, python-format +msgid "" +"mentioned you in a quote from " +"%(book_title)s" +msgstr "" +"在 %(book_title)s 的引用 中提到" +"了你" + +#: bookwyrm/templates/notifications.html:70 +#, python-format +msgid "mentioned you in a status" +msgstr "在 状态 中提到了你" + +#: bookwyrm/templates/notifications.html:75 +#, python-format +msgid "" +"replied to your review of %(book_title)s" +msgstr "" +"回复 了你的 对 " +"%(book_title)s 的书评" + +#: bookwyrm/templates/notifications.html:77 +#, python-format +msgid "" +"replied to your comment on %(book_title)s" +msgstr "" +"回复 了你的 对 " +"%(book_title)s 的评论" + +#: bookwyrm/templates/notifications.html:79 +#, python-format +msgid "" +"replied to your quote from %(book_title)s" +msgstr "" +"回复 了你 对 " +"%(book_title)s 中的引用" + +#: bookwyrm/templates/notifications.html:81 +#, python-format +msgid "" +"replied to your status" +msgstr "" +"回复 了你的 状态" +"" + +#: bookwyrm/templates/notifications.html:85 +msgid "followed you" +msgstr "关注了你" + +#: bookwyrm/templates/notifications.html:88 +msgid "sent you a follow request" +msgstr "向你发送了关注请求" + +#: bookwyrm/templates/notifications.html:94 +#, python-format +msgid "" +"boosted your review of %(book_title)s" +msgstr "" +"转发了你的 %(book_title)s 的书评" + +#: bookwyrm/templates/notifications.html:96 +#, python-format +msgid "" +"boosted your comment on%(book_title)s" +msgstr "" +"转发了你的 %(book_title)s 的评论" + +#: bookwyrm/templates/notifications.html:98 +#, python-format +msgid "" +"boosted your quote from %(book_title)s" +msgstr "" +"转发了你的 %(book_title)s 的引用" + +#: bookwyrm/templates/notifications.html:100 +#, python-format +msgid "boosted your status" +msgstr "转发了你的 状态" + +#: bookwyrm/templates/notifications.html:104 +#, python-format +msgid "" +" added %(book_title)s to your list " +"\"%(list_name)s\"" +msgstr "" +" 添加了 %(book_title)s 到你的列表 " +"\"%(list_name)s\"" + +#: bookwyrm/templates/notifications.html:106 +#, python-format +msgid "" +" suggested adding %(book_title)s to " +"your list \"%(list_name)s\"" +msgstr "" +" 推荐添加 %(book_title)s 到你的列表 " +"\"%(list_name)s\"" + +#: bookwyrm/templates/notifications.html:110 +#, python-format +msgid "Your import completed." +msgstr "你的 导入 已完成。" + +#: bookwyrm/templates/notifications.html:113 +#, python-format +msgid "A new report needs moderation." +msgstr "有新的 报告 需要仲裁。" + +#: bookwyrm/templates/notifications.html:139 +msgid "You're all caught up!" +msgstr "你什么也没错过!" + +#: bookwyrm/templates/password_reset.html:23 +#: bookwyrm/templates/preferences/change_password.html:18 +msgid "Confirm password:" +msgstr "确认密码:" + +#: bookwyrm/templates/password_reset_request.html:14 +msgid "A link to reset your password will be sent to your email address" +msgstr "重设你的密码的链接将会被发送到你的邮箱地址" + +#: bookwyrm/templates/password_reset_request.html:28 +msgid "Reset password" +msgstr "重设密码" + +#: bookwyrm/templates/preferences/blocks.html:4 +#: bookwyrm/templates/preferences/blocks.html:7 +#: bookwyrm/templates/preferences/preferences_layout.html:23 +msgid "Blocked Users" +msgstr "屏蔽的用户" + +#: bookwyrm/templates/preferences/blocks.html:12 +msgid "No users currently blocked." +msgstr "当前没有被屏蔽的用户。" + +#: bookwyrm/templates/preferences/change_password.html:4 +#: bookwyrm/templates/preferences/change_password.html:7 +#: bookwyrm/templates/preferences/change_password.html:21 +#: bookwyrm/templates/preferences/preferences_layout.html:17 +msgid "Change Password" +msgstr "更改密码" + +#: bookwyrm/templates/preferences/change_password.html:14 +msgid "New password:" +msgstr "新密码:" + +#: bookwyrm/templates/preferences/edit_user.html:4 +#: bookwyrm/templates/preferences/edit_user.html:7 +msgid "Edit Profile" +msgstr "编辑个人资料" + +#: bookwyrm/templates/preferences/edit_user.html:46 +msgid "Show set reading goal prompt in feed:" +msgstr "在消息流中显示设置阅读目标的提示:" + +#: bookwyrm/templates/preferences/edit_user.html:62 +#, python-format +msgid "" +"Your account will show up in the directory, and may " +"be recommended to other BookWyrm users." +msgstr "" +"你的帐号会显示在 目录 中,并可能受其它 BookWyrm 用户" +"推荐。" + +#: bookwyrm/templates/preferences/edit_user.html:65 +msgid "Preferred Timezone: " +msgstr "偏好的时区:" + +#: bookwyrm/templates/preferences/preferences_layout.html:11 +msgid "Account" +msgstr "帐号" + +#: bookwyrm/templates/preferences/preferences_layout.html:14 +msgid "Profile" +msgstr "个人资料" + +#: bookwyrm/templates/preferences/preferences_layout.html:20 +msgid "Relationships" +msgstr "关系" + +#: bookwyrm/templates/search_results.html:33 +msgid "Didn't find what you were looking for?" +msgstr "没有找到你想找的?" + +#: bookwyrm/templates/search_results.html:35 +msgid "Show results from other catalogues" +msgstr "显示其它类别的结果" + +#: bookwyrm/templates/search_results.html:62 +msgid "Hide results from other catalogues" +msgstr "隐藏其它类别的结果" + +#: bookwyrm/templates/search_results.html:74 +msgid "Matching Users" +msgstr "匹配的用户" + +#: bookwyrm/templates/search_results.html:93 +#, python-format +msgid "No lists found for \"%(query)s\"" +msgstr "没有找到 \"%(query)s\" 的列表" + +#: bookwyrm/templates/settings/admin_layout.html:4 +msgid "Administration" +msgstr "管理" + +#: bookwyrm/templates/settings/admin_layout.html:22 +msgid "Manage Users" +msgstr "管理用户" + +#: bookwyrm/templates/settings/admin_layout.html:26 +#: bookwyrm/templates/user_admin/user_admin.html:3 +#: bookwyrm/templates/user_admin/user_admin.html:10 +msgid "Users" +msgstr "用户" + +#: bookwyrm/templates/settings/admin_layout.html:39 +#: bookwyrm/templates/settings/federation.html:3 +#: bookwyrm/templates/settings/federation.html:5 +msgid "Federated Servers" +msgstr "互联的服务器" + +#: bookwyrm/templates/settings/admin_layout.html:44 +msgid "Instance Settings" +msgstr "实例设置" + +#: bookwyrm/templates/settings/admin_layout.html:48 +#: bookwyrm/templates/settings/site.html:4 +#: bookwyrm/templates/settings/site.html:6 +msgid "Site Settings" +msgstr "站点设置" + +#: bookwyrm/templates/settings/admin_layout.html:51 +#: bookwyrm/templates/settings/site.html:13 +msgid "Instance Info" +msgstr "实例信息" + +#: bookwyrm/templates/settings/admin_layout.html:52 +#: bookwyrm/templates/settings/site.html:39 +msgid "Images" +msgstr "图像" + +#: bookwyrm/templates/settings/admin_layout.html:53 +#: bookwyrm/templates/settings/site.html:59 +msgid "Footer Content" +msgstr "页脚内容" + +#: bookwyrm/templates/settings/admin_layout.html:54 +#: bookwyrm/templates/settings/site.html:77 +msgid "Registration" +msgstr "注册" + +#: bookwyrm/templates/settings/edit_server.html:3 +#: bookwyrm/templates/settings/edit_server.html:6 +#: bookwyrm/templates/settings/edit_server.html:20 +#: bookwyrm/templates/settings/federation.html:9 +#: bookwyrm/templates/settings/federation.html:10 +#: bookwyrm/templates/settings/server_blocklist.html:3 +#: bookwyrm/templates/settings/server_blocklist.html:20 +#, fuzzy +#| msgid "Add cover" +msgid "Add server" +msgstr "添加封面" + +#: bookwyrm/templates/settings/edit_server.html:7 +#: bookwyrm/templates/settings/federated_server.html:12 +#: bookwyrm/templates/settings/server_blocklist.html:7 +msgid "Back to server list" +msgstr "回到服务器列表" + +#: bookwyrm/templates/settings/edit_server.html:16 +#: bookwyrm/templates/settings/server_blocklist.html:16 +#, fuzzy +#| msgid "Import book" +msgid "Import block list" +msgstr "导入书目" + +#: bookwyrm/templates/settings/edit_server.html:30 +#, fuzzy +#| msgid "Instance Name:" +msgid "Instance:" +msgstr "实例名称" + +#: bookwyrm/templates/settings/edit_server.html:37 +#: bookwyrm/templates/settings/federated_server.html:29 +#: bookwyrm/templates/user_admin/user_info.html:34 +msgid "Status:" +msgstr "状态:" + +#: bookwyrm/templates/settings/edit_server.html:41 +#: bookwyrm/templates/settings/federated_server.html:9 +#, fuzzy +#| msgid "Block" +msgid "Blocked" +msgstr "屏蔽" + +#: bookwyrm/templates/settings/edit_server.html:48 +#: bookwyrm/templates/settings/federated_server.html:21 +#: bookwyrm/templates/user_admin/user_info.html:26 +msgid "Software:" +msgstr "软件:" + +#: bookwyrm/templates/settings/edit_server.html:55 +#: bookwyrm/templates/settings/federated_server.html:25 +#: bookwyrm/templates/user_admin/user_info.html:30 +msgid "Version:" +msgstr "版本:" + +#: bookwyrm/templates/settings/edit_server.html:64 +msgid "Notes:" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:18 +msgid "Details" +msgstr "详细" + +#: bookwyrm/templates/settings/federated_server.html:36 +#: bookwyrm/templates/user/user_layout.html:50 +msgid "Activity" +msgstr "活动" + +#: bookwyrm/templates/settings/federated_server.html:39 +msgid "Users:" +msgstr "用户:" + +#: bookwyrm/templates/settings/federated_server.html:42 +#: bookwyrm/templates/settings/federated_server.html:49 +msgid "View all" +msgstr "查看全部" + +#: bookwyrm/templates/settings/federated_server.html:46 +msgid "Reports:" +msgstr "报告:" + +#: bookwyrm/templates/settings/federated_server.html:53 +msgid "Followed by us:" +msgstr "我们关注了的:" + +#: bookwyrm/templates/settings/federated_server.html:59 +msgid "Followed by them:" +msgstr "TA 们关注了的:" + +#: bookwyrm/templates/settings/federated_server.html:65 +msgid "Blocked by us:" +msgstr "我们所屏蔽的:" + +#: bookwyrm/templates/settings/federated_server.html:77 +#: bookwyrm/templates/user_admin/user_info.html:39 +msgid "Notes" +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:80 +#, fuzzy +#| msgid "Edit Book" +msgid "Edit" +msgstr "编辑书目" + +#: bookwyrm/templates/settings/federated_server.html:100 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:3 +msgid "Actions" +msgstr "动作" + +#: bookwyrm/templates/settings/federated_server.html:104 +#: bookwyrm/templates/snippets/block_button.html:5 +msgid "Block" +msgstr "屏蔽" + +#: bookwyrm/templates/settings/federated_server.html:105 +msgid "All users from this instance will be deactivated." +msgstr "" + +#: bookwyrm/templates/settings/federated_server.html:110 +#: bookwyrm/templates/snippets/block_button.html:10 +msgid "Un-block" +msgstr "取消屏蔽" + +#: bookwyrm/templates/settings/federated_server.html:111 +msgid "All users from this instance will be re-activated." +msgstr "" + +#: bookwyrm/templates/settings/federation.html:20 +#: bookwyrm/templates/user_admin/server_filter.html:5 +msgid "Server name" +msgstr "服务器名称" + +#: bookwyrm/templates/settings/federation.html:24 +msgid "Date federated" +msgstr "跨站日期" + +#: bookwyrm/templates/settings/federation.html:28 +msgid "Software" +msgstr "软件" + +#: bookwyrm/templates/settings/federation.html:31 +#: bookwyrm/templates/settings/manage_invite_requests.html:44 +#: bookwyrm/templates/settings/status_filter.html:5 +#: bookwyrm/templates/user_admin/user_admin.html:34 +msgid "Status" +msgstr "状态" + +#: bookwyrm/templates/settings/manage_invite_requests.html:4 +#: bookwyrm/templates/settings/manage_invite_requests.html:11 +#: bookwyrm/templates/settings/manage_invite_requests.html:25 +#: bookwyrm/templates/settings/manage_invites.html:11 +msgid "Invite Requests" +msgstr "邀请请求" + +#: bookwyrm/templates/settings/manage_invite_requests.html:23 +msgid "Ignored Invite Requests" +msgstr "已忽略的邀请请求" + +#: bookwyrm/templates/settings/manage_invite_requests.html:35 +#, fuzzy +#| msgid "Date federated" +msgid "Date requested" +msgstr "跨站日期" + +#: bookwyrm/templates/settings/manage_invite_requests.html:39 +#, fuzzy +#| msgid "Accepted" +msgid "Date accepted" +msgstr "已接受" + +#: bookwyrm/templates/settings/manage_invite_requests.html:42 +msgid "Email" +msgstr "邮箱" + +#: bookwyrm/templates/settings/manage_invite_requests.html:47 +msgid "Action" +msgstr "动作" + +#: bookwyrm/templates/settings/manage_invite_requests.html:50 +msgid "No requests" +msgstr "没有请求" + +#: bookwyrm/templates/settings/manage_invite_requests.html:59 +#: bookwyrm/templates/settings/status_filter.html:16 +msgid "Accepted" +msgstr "已接受" + +#: bookwyrm/templates/settings/manage_invite_requests.html:61 +#: bookwyrm/templates/settings/status_filter.html:12 +msgid "Sent" +msgstr "已发送" + +#: bookwyrm/templates/settings/manage_invite_requests.html:63 +#: bookwyrm/templates/settings/status_filter.html:8 +msgid "Requested" +msgstr "已请求" + +#: bookwyrm/templates/settings/manage_invite_requests.html:73 +msgid "Send invite" +msgstr "发送请求" + +#: bookwyrm/templates/settings/manage_invite_requests.html:75 +msgid "Re-send invite" +msgstr "重新发送请求" + +#: bookwyrm/templates/settings/manage_invite_requests.html:95 +msgid "Ignore" +msgstr "忽略" + +#: bookwyrm/templates/settings/manage_invite_requests.html:97 +msgid "Un-ignore" +msgstr "取消忽略" + +#: bookwyrm/templates/settings/manage_invite_requests.html:108 +msgid "Back to pending requests" +msgstr "回到待处理的请求" + +#: bookwyrm/templates/settings/manage_invite_requests.html:110 +msgid "View ignored requests" +msgstr "查看忽略的请求" + +#: bookwyrm/templates/settings/manage_invites.html:21 +msgid "Generate New Invite" +msgstr "生成新的邀请" + +#: bookwyrm/templates/settings/manage_invites.html:27 +msgid "Expiry:" +msgstr "过期:" + +#: bookwyrm/templates/settings/manage_invites.html:33 +msgid "Use limit:" +msgstr "使用限制:" + +#: bookwyrm/templates/settings/manage_invites.html:40 +msgid "Create Invite" +msgstr "创建邀请" + +#: bookwyrm/templates/settings/manage_invites.html:47 +msgid "Link" +msgstr "链接" + +#: bookwyrm/templates/settings/manage_invites.html:48 +msgid "Expires" +msgstr "过期" + +#: bookwyrm/templates/settings/manage_invites.html:49 +msgid "Max uses" +msgstr "最大使用次数" + +#: bookwyrm/templates/settings/manage_invites.html:50 +msgid "Times used" +msgstr "已使用次数" + +#: bookwyrm/templates/settings/manage_invites.html:53 +msgid "No active invites" +msgstr "无有效的邀请" + +#: bookwyrm/templates/settings/server_blocklist.html:6 +#, fuzzy +#| msgid "Import Books" +msgid "Import Blocklist" +msgstr "导入书目" + +#: bookwyrm/templates/settings/server_blocklist.html:26 +#: bookwyrm/templates/snippets/goal_progress.html:5 +msgid "Success!" +msgstr "成功!" + +#: bookwyrm/templates/settings/server_blocklist.html:30 +#, fuzzy +#| msgid "Successfully imported" +msgid "Successfully blocked:" +msgstr "成功导入了" + +#: bookwyrm/templates/settings/server_blocklist.html:32 +msgid "Failed:" +msgstr "" + +#: bookwyrm/templates/settings/site.html:15 +msgid "Instance Name:" +msgstr "实例名称" + +#: bookwyrm/templates/settings/site.html:19 +msgid "Tagline:" +msgstr "标语" + +#: bookwyrm/templates/settings/site.html:23 +msgid "Instance description:" +msgstr "实例描述:" + +#: bookwyrm/templates/settings/site.html:27 +msgid "Code of conduct:" +msgstr "行为准则:" + +#: bookwyrm/templates/settings/site.html:31 +msgid "Privacy Policy:" +msgstr "隐私政策:" + +#: bookwyrm/templates/settings/site.html:42 +msgid "Logo:" +msgstr "图标:" + +#: bookwyrm/templates/settings/site.html:46 +msgid "Logo small:" +msgstr "小号图标:" + +#: bookwyrm/templates/settings/site.html:50 +msgid "Favicon:" +msgstr "Favicon:" + +#: bookwyrm/templates/settings/site.html:61 +msgid "Support link:" +msgstr "支持链接:" + +#: bookwyrm/templates/settings/site.html:65 +msgid "Support title:" +msgstr "支持标题:" + +#: bookwyrm/templates/settings/site.html:69 +msgid "Admin email:" +msgstr "管理员邮件:" + +#: bookwyrm/templates/settings/site.html:79 +msgid "Allow registration:" +msgstr "允许注册:" + +#: bookwyrm/templates/settings/site.html:83 +msgid "Allow invite requests:" +msgstr "允许请求邀请:" + +#: bookwyrm/templates/settings/site.html:87 +msgid "Registration closed text:" +msgstr "注册关闭文字:" + +#: bookwyrm/templates/snippets/book_cover.html:20 +#: bookwyrm/templates/snippets/search_result_text.html:10 +msgid "No cover" +msgstr "没有封面" + +#: bookwyrm/templates/snippets/book_titleby.html:4 +#, python-format +msgid "%(title)s by " +msgstr "%(title)s 来自" + +#: bookwyrm/templates/snippets/boost_button.html:8 +#: bookwyrm/templates/snippets/boost_button.html:9 +#, fuzzy +#| msgid "boosted" +msgid "Boost" +msgstr "转发了" + +#: bookwyrm/templates/snippets/boost_button.html:15 +#: bookwyrm/templates/snippets/boost_button.html:16 +#, fuzzy +#| msgid "Un-boost status" +msgid "Un-boost" +msgstr "取消转发状态" + +#: bookwyrm/templates/snippets/content_warning_field.html:3 +msgid "Spoiler alert:" +msgstr "剧透警告:" + +#: bookwyrm/templates/snippets/content_warning_field.html:10 +msgid "Spoilers ahead!" +msgstr "前有剧透!" + +#: bookwyrm/templates/snippets/create_status.html:17 +msgid "Review" +msgstr "书评" + +#: bookwyrm/templates/snippets/create_status.html:39 +msgid "Quote" +msgstr "引用" + +#: bookwyrm/templates/snippets/create_status_form.html:20 +msgid "Comment:" +msgstr "评论:" + +#: bookwyrm/templates/snippets/create_status_form.html:22 +msgid "Quote:" +msgstr "引用:" + +#: bookwyrm/templates/snippets/create_status_form.html:24 +msgid "Review:" +msgstr "书评:" + +#: bookwyrm/templates/snippets/create_status_form.html:42 +#: bookwyrm/templates/snippets/status/layout.html:30 +#: bookwyrm/templates/snippets/status/layout.html:48 +#: bookwyrm/templates/snippets/status/layout.html:49 +msgid "Reply" +msgstr "回复" + +#: bookwyrm/templates/snippets/create_status_form.html:67 +#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:16 +msgid "Progress:" +msgstr "进度:" + +#: bookwyrm/templates/snippets/create_status_form.html:75 +#: bookwyrm/templates/snippets/readthrough_form.html:22 +#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:30 +msgid "pages" +msgstr "页数" + +#: bookwyrm/templates/snippets/create_status_form.html:76 +#: bookwyrm/templates/snippets/readthrough_form.html:23 +#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:31 +msgid "percent" +msgstr "百分比" + +#: bookwyrm/templates/snippets/create_status_form.html:82 +#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:36 +#, python-format +msgid "of %(pages)s pages" +msgstr "全书 %(pages)s 页" + +#: bookwyrm/templates/snippets/create_status_form.html:97 +msgid "Include spoiler alert" +msgstr "加入剧透警告" + +#: bookwyrm/templates/snippets/create_status_form.html:104 +#: bookwyrm/templates/snippets/privacy-icons.html:15 +#: bookwyrm/templates/snippets/privacy-icons.html:16 +#: bookwyrm/templates/snippets/privacy_select.html:19 +msgid "Private" +msgstr "私密" + +#: bookwyrm/templates/snippets/create_status_form.html:115 +msgid "Post" +msgstr "发布" + +#: bookwyrm/templates/snippets/delete_readthrough_modal.html:4 +msgid "Delete these read dates?" +msgstr "删除这些阅读日期吗?" + +#: bookwyrm/templates/snippets/delete_readthrough_modal.html:7 +#, python-format +msgid "" +"You are deleting this readthrough and its %(count)s associated progress " +"updates." +msgstr "你正要删除这篇阅读经过以及与之相关的 %(count)s 次进度更新。" + +#: bookwyrm/templates/snippets/delete_readthrough_modal.html:15 +#: bookwyrm/templates/snippets/follow_request_buttons.html:13 +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:391 +msgid "Delete" +msgstr "删除" + +#: bookwyrm/templates/snippets/fav_button.html:7 +#: bookwyrm/templates/snippets/fav_button.html:9 +msgid "Like" +msgstr "" + +#: bookwyrm/templates/snippets/fav_button.html:15 +#: bookwyrm/templates/snippets/fav_button.html:16 +#, fuzzy +#| msgid "Un-like status" +msgid "Un-like" +msgstr "取消喜欢状态" + +#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:7 +msgid "Show filters" +msgstr "显示过滤器" + +#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:9 +msgid "Hide filters" +msgstr "隐藏过滤器" + +#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:22 +msgid "Apply filters" +msgstr "应用过滤器" + +#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:26 +msgid "Clear filters" +msgstr "清除过滤器" + +#: bookwyrm/templates/snippets/follow_button.html:12 +msgid "Follow" +msgstr "关注" + +#: bookwyrm/templates/snippets/follow_button.html:18 +msgid "Undo follow request" +msgstr "撤回关注请求" + +#: bookwyrm/templates/snippets/follow_button.html:20 +msgid "Unfollow" +msgstr "取消关注" + +#: bookwyrm/templates/snippets/follow_request_buttons.html:8 +msgid "Accept" +msgstr "接受" + +#: bookwyrm/templates/snippets/form_rate_stars.html:20 +#: bookwyrm/templates/snippets/stars.html:13 +msgid "No rating" +msgstr "没有评价" + +#: bookwyrm/templates/snippets/generated_status/goal.html:1 +#, python-format +msgid "set a goal to read %(counter)s book in %(year)s" +msgid_plural "set a goal to read %(counter)s books in %(year)s" +msgstr[0] "设定了在 %(year)s 内要读 %(counter)s 本书的目标" + +#: bookwyrm/templates/snippets/generated_status/rating.html:3 +#, python-format +msgid "" +"Rated %(title)s: %(display_rating)s star" +msgid_plural "" +"Rated %(title)s: %(display_rating)s stars" +msgstr[0] "" +"为 %(title)s 打了分: %(display_rating)s 星" + +#: bookwyrm/templates/snippets/generated_status/review_pure_name.html:4 +#, python-format +msgid "" +"Review of \"%(book_title)s\" (%(display_rating)s star): %(review_title)s" +msgid_plural "" +"Review of \"%(book_title)s\" (%(display_rating)s stars): %(review_title)s" +msgstr[0] "" +"\"%(book_title)s\" 的书评(%(display_rating)s 星): %(review_title)s" + +#: bookwyrm/templates/snippets/generated_status/review_pure_name.html:8 +#, python-format +msgid "Review of \"%(book_title)s\": %(review_title)s" +msgstr "\"%(book_title)s\" 的书评: %(review_title)s" + +#: bookwyrm/templates/snippets/goal_card.html:23 +#, python-format +msgid "" +"You can set or change your reading goal any time from your profile page" +msgstr "" +"你可以在任何时候从你的个人资料页面 中设置或改变你的" +"阅读目标" + +#: bookwyrm/templates/snippets/goal_form.html:9 +msgid "Reading goal:" +msgstr "阅读目标:" + +#: bookwyrm/templates/snippets/goal_form.html:14 +msgid "books" +msgstr "本书" + +#: bookwyrm/templates/snippets/goal_form.html:19 +msgid "Goal privacy:" +msgstr "目标隐私:" + +#: bookwyrm/templates/snippets/goal_form.html:26 +#: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:37 +#: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:29 +#: bookwyrm/templates/snippets/shelve_button/want_to_read_modal.html:20 +msgid "Post to feed" +msgstr "发布到消息流中" + +#: bookwyrm/templates/snippets/goal_form.html:30 +msgid "Set goal" +msgstr "设置目标" + +#: bookwyrm/templates/snippets/goal_progress.html:7 +#, python-format +msgid "%(percent)s%% complete!" +msgstr "完成了 %(percent)s%% !" + +#: bookwyrm/templates/snippets/goal_progress.html:10 +#, python-format +msgid "" +"You've read %(read_count)s of %(goal_count)s books." +msgstr "" +"你已经阅读了 %(goal_count)s 本书中的 %(read_count)s 本。" + +#: bookwyrm/templates/snippets/goal_progress.html:12 +#, python-format +msgid "" +"%(username)s has read %(read_count)s of %(goal_count)s " +"books." +msgstr "" +"%(username)s 已经阅读了 %(goal_count)s 本书中的 " +"%(read_count)s 本。" + +#: bookwyrm/templates/snippets/page_text.html:4 +#, python-format +msgid "page %(page)s of %(total_pages)s" +msgstr "%(total_pages)s 页中的第 %(page)s 页" + +#: bookwyrm/templates/snippets/page_text.html:6 +#, python-format +msgid "page %(page)s" +msgstr "第 %(page)s 页" + +#: bookwyrm/templates/snippets/pagination.html:12 +msgid "Previous" +msgstr "往前" + +#: bookwyrm/templates/snippets/pagination.html:23 +msgid "Next" +msgstr "往后" + +#: bookwyrm/templates/snippets/privacy-icons.html:3 +#: bookwyrm/templates/snippets/privacy-icons.html:4 +#: bookwyrm/templates/snippets/privacy_select.html:10 +msgid "Public" +msgstr "公开" + +#: bookwyrm/templates/snippets/privacy-icons.html:7 +#: bookwyrm/templates/snippets/privacy-icons.html:8 +#: bookwyrm/templates/snippets/privacy_select.html:13 +msgid "Unlisted" +msgstr "不公开" + +#: bookwyrm/templates/snippets/privacy-icons.html:12 +msgid "Followers-only" +msgstr "仅关注者" + +#: bookwyrm/templates/snippets/privacy_select.html:6 +msgid "Post privacy" +msgstr "发文隐私" + +#: bookwyrm/templates/snippets/privacy_select.html:16 +#: bookwyrm/templates/user/followers.html:13 +msgid "Followers" +msgstr "关注者" + +#: bookwyrm/templates/snippets/rate_action.html:4 +msgid "Leave a rating" +msgstr "留下评价" + +#: bookwyrm/templates/snippets/rate_action.html:19 +msgid "Rate" +msgstr "评价" + +#: bookwyrm/templates/snippets/readthrough.html:8 +msgid "Progress Updates:" +msgstr "进度更新:" + +#: bookwyrm/templates/snippets/readthrough.html:14 +msgid "finished" +msgstr "已完成" + +#: bookwyrm/templates/snippets/readthrough.html:25 +msgid "Show all updates" +msgstr "显示所有更新" + +#: bookwyrm/templates/snippets/readthrough.html:41 +msgid "Delete this progress update" +msgstr "删除此进度更新" + +#: bookwyrm/templates/snippets/readthrough.html:51 +msgid "started" +msgstr "已开始" + +#: bookwyrm/templates/snippets/readthrough.html:57 +#: bookwyrm/templates/snippets/readthrough.html:71 +msgid "Edit read dates" +msgstr "编辑阅读日期" + +#: bookwyrm/templates/snippets/readthrough.html:61 +msgid "Delete these read dates" +msgstr "删除这些阅读日期" + +#: bookwyrm/templates/snippets/readthrough_form.html:7 +#: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:19 +#: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:17 +msgid "Started reading" +msgstr "已开始阅读" + +#: bookwyrm/templates/snippets/readthrough_form.html:14 +msgid "Progress" +msgstr "进度" + +#: bookwyrm/templates/snippets/readthrough_form.html:30 +#: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:25 +msgid "Finished reading" +msgstr "已完成阅读" + +#: bookwyrm/templates/snippets/register_form.html:32 +msgid "Sign Up" +msgstr "注册" + +#: bookwyrm/templates/snippets/report_button.html:5 +msgid "Report" +msgstr "报告" + +#: bookwyrm/templates/snippets/rss_title.html:5 +#: bookwyrm/templates/snippets/status/status_header.html:34 +msgid "rated" +msgstr "评价了" + +#: bookwyrm/templates/snippets/rss_title.html:7 +#: bookwyrm/templates/snippets/status/status_header.html:36 +msgid "reviewed" +msgstr "写了书评给" + +#: bookwyrm/templates/snippets/rss_title.html:9 +#: bookwyrm/templates/snippets/status/status_header.html:38 +msgid "commented on" +msgstr "评论了" + +#: bookwyrm/templates/snippets/rss_title.html:11 +#: bookwyrm/templates/snippets/status/status_header.html:40 +msgid "quoted" +msgstr "引用了" + +#: bookwyrm/templates/snippets/search_result_text.html:22 +#, python-format +msgid "by %(author)s" +msgstr "由 %(author)s 所著" + +#: bookwyrm/templates/snippets/search_result_text.html:30 +msgid "Import book" +msgstr "导入书目" + +#: bookwyrm/templates/snippets/shelf_selector.html:4 +msgid "Move book" +msgstr "移动书目" + +#: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:5 +#, python-format +msgid "Finish \"%(book_title)s\"" +msgstr "完成 \"%(book_title)s\"" + +#: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:5 +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:34 +msgid "Update progress" +msgstr "更新进度" + +#: bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown.html:5 +msgid "More shelves" +msgstr "更多书架" + +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:8 +msgid "Start reading" +msgstr "开始阅读" + +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:13 +msgid "Finish reading" +msgstr "完成阅读" + +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:16 +#: bookwyrm/templates/snippets/shelve_button/want_to_read_modal.html:26 +msgid "Want to read" +msgstr "想要阅读" + +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:45 +#, python-format +msgid "Remove from %(name)s" +msgstr "从 %(name)s 移除" + +#: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:5 +#, python-format +msgid "Start \"%(book_title)s\"" +msgstr "开始 \"%(book_title)s\"" + +#: bookwyrm/templates/snippets/shelve_button/want_to_read_modal.html:5 +#, python-format +msgid "Want to Read \"%(book_title)s\"" +msgstr "想要阅读 \"%(book_title)s\"" + +#: bookwyrm/templates/snippets/status/content_status.html:67 +#: bookwyrm/templates/snippets/trimmed_text.html:14 +msgid "Show more" +msgstr "显示更多" + +#: bookwyrm/templates/snippets/status/content_status.html:82 +#: bookwyrm/templates/snippets/trimmed_text.html:29 +msgid "Show less" +msgstr "显示更少" + +#: bookwyrm/templates/snippets/status/content_status.html:112 +msgid "Open image in new window" +msgstr "在新窗口中打开图像" + +#: bookwyrm/templates/snippets/status/layout.html:22 +#: bookwyrm/templates/snippets/status/status_options.html:17 +msgid "Delete status" +msgstr "删除发文" + +#: bookwyrm/templates/snippets/status/layout.html:52 +#: bookwyrm/templates/snippets/status/layout.html:53 +msgid "Boost status" +msgstr "转发状态" + +#: bookwyrm/templates/snippets/status/layout.html:56 +#: bookwyrm/templates/snippets/status/layout.html:57 +msgid "Like status" +msgstr "喜欢状态" + +#: bookwyrm/templates/snippets/status/status.html:9 +msgid "boosted" +msgstr "转发了" + +#: bookwyrm/templates/snippets/status/status_header.html:44 +#, python-format +msgid "" +"replied to %(username)s's status" +msgstr "" +"回复了 %(username)s状态" + +#: bookwyrm/templates/snippets/status/status_options.html:7 +#: bookwyrm/templates/snippets/user_options.html:7 +msgid "More options" +msgstr "更多选项" + +#: bookwyrm/templates/snippets/status/status_options.html:26 +#, fuzzy +#| msgid "Delete these read dates" +msgid "Delete & re-draft" +msgstr "删除这些阅读日期" + +#: bookwyrm/templates/snippets/status/status_options.html:35 +#: bookwyrm/templates/snippets/user_options.html:13 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:6 +msgid "Send direct message" +msgstr "发送私信" + +#: bookwyrm/templates/snippets/switch_edition_button.html:5 +msgid "Switch to this edition" +msgstr "切换到此版本" + +#: bookwyrm/templates/snippets/table-sort-header.html:6 +msgid "Sorted ascending" +msgstr "升序排序" + +#: bookwyrm/templates/snippets/table-sort-header.html:10 +msgid "Sorted descending" +msgstr "降序排序" + +#: bookwyrm/templates/user/books_header.html:5 +#, python-format +msgid "%(username)s's books" +msgstr "%(username)s 的书目" + +#: bookwyrm/templates/user/create_shelf_form.html:5 +#: bookwyrm/templates/user/create_shelf_form.html:22 +msgid "Create Shelf" +msgstr "创建书架" + +#: bookwyrm/templates/user/edit_shelf_form.html:5 +msgid "Edit Shelf" +msgstr "编辑书架" + +#: bookwyrm/templates/user/edit_shelf_form.html:26 +msgid "Update shelf" +msgstr "更新书架" + +#: bookwyrm/templates/user/followers.html:7 +#: bookwyrm/templates/user/following.html:7 +#: bookwyrm/templates/user/user.html:10 +msgid "User Profile" +msgstr "用户个人资料" + +#: bookwyrm/templates/user/followers.html:29 +#, python-format +msgid "%(username)s has no followers" +msgstr "%(username)s 没有关注者" + +#: bookwyrm/templates/user/following.html:13 +msgid "Following" +msgstr "正在关注" + +#: bookwyrm/templates/user/following.html:29 +#, python-format +msgid "%(username)s isn't following any users" +msgstr "%(username)s 没有关注任何用户" + +#: bookwyrm/templates/user/lists.html:11 +#, python-format +msgid "Lists: %(username)s" +msgstr "列表: %(username)s" + +#: bookwyrm/templates/user/lists.html:17 bookwyrm/templates/user/lists.html:29 +msgid "Create list" +msgstr "创建列表" + +#: bookwyrm/templates/user/shelf.html:24 bookwyrm/views/shelf.py:51 +msgid "All books" +msgstr "所有书目" + +#: bookwyrm/templates/user/shelf.html:37 +msgid "Create shelf" +msgstr "创建书架" + +#: bookwyrm/templates/user/shelf.html:58 +msgid "Edit shelf" +msgstr "编辑书架" + +#: bookwyrm/templates/user/shelf.html:77 bookwyrm/templates/user/shelf.html:99 +msgid "Shelved" +msgstr "上架时间" + +#: bookwyrm/templates/user/shelf.html:78 bookwyrm/templates/user/shelf.html:103 +msgid "Started" +msgstr "开始时间" + +#: bookwyrm/templates/user/shelf.html:79 bookwyrm/templates/user/shelf.html:106 +msgid "Finished" +msgstr "完成时间" + +#: bookwyrm/templates/user/shelf.html:132 +msgid "This shelf is empty." +msgstr "此书架是空的。" + +#: bookwyrm/templates/user/shelf.html:138 +msgid "Delete shelf" +msgstr "删除书架" + +#: bookwyrm/templates/user/user.html:16 +msgid "Edit profile" +msgstr "编辑个人资料" + +#: bookwyrm/templates/user/user.html:34 +#, python-format +msgid "View all %(size)s" +msgstr "查看所有 %(size)s 本" + +#: bookwyrm/templates/user/user.html:47 +msgid "View all books" +msgstr "查看所有书目" + +#: bookwyrm/templates/user/user.html:59 +#, python-format +msgid "Set a reading goal for %(year)s" +msgstr "设定 %(year)s 的阅读目标" + +#: bookwyrm/templates/user/user.html:65 +msgid "User Activity" +msgstr "用户活动" + +#: bookwyrm/templates/user/user.html:68 +msgid "RSS feed" +msgstr "RSS 流" + +#: bookwyrm/templates/user/user.html:79 +msgid "No activities yet!" +msgstr "还没有活动!" + +#: bookwyrm/templates/user/user_layout.html:32 +msgid "Follow Requests" +msgstr "关注请求" + +#: bookwyrm/templates/user/user_layout.html:56 +msgid "Reading Goal" +msgstr "阅读目标" + +#: bookwyrm/templates/user/user_layout.html:68 +msgid "Books" +msgstr "书目" + +#: bookwyrm/templates/user/user_preview.html:13 +#, python-format +msgid "Joined %(date)s" +msgstr "在 %(date)s 加入" + +#: bookwyrm/templates/user/user_preview.html:15 +#, python-format +msgid "%(counter)s follower" +msgid_plural "%(counter)s followers" +msgstr[0] "%(counter)s 个关注者" + +#: bookwyrm/templates/user/user_preview.html:16 +#, python-format +msgid "%(counter)s following" +msgstr "关注着 %(counter)s 人" + +#: bookwyrm/templates/user_admin/user.html:11 +#, fuzzy +#| msgid "Back to reports" +msgid "Back to users" +msgstr "回到报告" + +#: bookwyrm/templates/user_admin/user_admin.html:7 +#, python-format +msgid "Users: %(server_name)s" +msgstr "用户: %(server_name)s" + +#: bookwyrm/templates/user_admin/user_admin.html:22 +#: bookwyrm/templates/user_admin/username_filter.html:5 +msgid "Username" +msgstr "用户名" + +#: bookwyrm/templates/user_admin/user_admin.html:26 +msgid "Date Added" +msgstr "添加日期:" + +#: bookwyrm/templates/user_admin/user_admin.html:30 +msgid "Last Active" +msgstr "最后或缺" + +#: bookwyrm/templates/user_admin/user_admin.html:38 +msgid "Remote server" +msgstr "移除服务器" + +#: bookwyrm/templates/user_admin/user_admin.html:47 +msgid "Active" +msgstr "活跃" + +#: bookwyrm/templates/user_admin/user_admin.html:47 +msgid "Inactive" +msgstr "停用" + +#: bookwyrm/templates/user_admin/user_admin.html:52 +#: bookwyrm/templates/user_admin/user_info.html:49 +msgid "Not set" +msgstr "未设置" + +#: bookwyrm/templates/user_admin/user_info.html:5 +#, fuzzy +#| msgid "Details" +msgid "User details" +msgstr "详细" + +#: bookwyrm/templates/user_admin/user_info.html:14 +msgid "View user profile" +msgstr "查看用户个人资料" + +#: bookwyrm/templates/user_admin/user_info.html:20 +#, fuzzy +#| msgid "Instance Settings" +msgid "Instance details" +msgstr "实例设置" + +#: bookwyrm/templates/user_admin/user_info.html:46 +msgid "View instance" +msgstr "" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:11 +msgid "Suspend user" +msgstr "" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:13 +msgid "Un-suspend user" +msgstr "" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:21 +msgid "Access level:" +msgstr "" + +#: bookwyrm/views/password.py:32 +msgid "No user with that email address was found." +msgstr "没有找到使用该邮箱的用户。" + +#: bookwyrm/views/password.py:41 +#, python-format +msgid "A password reset link sent to %s" +msgstr "密码重置连接已发送给 %s" + +#: venv3/lib/python3.8/site-packages/_pytest/config/argparsing.py:442 +#, python-format +msgid "ambiguous option: %(option)s could match %(matches)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/contrib/messages/apps.py:7 +#, fuzzy +#| msgid "All messages" +msgid "Messages" +msgstr "所有消息" + +#: venv3/lib/python3.8/site-packages/django/contrib/sitemaps/apps.py:7 +msgid "Site Maps" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/contrib/staticfiles/apps.py:9 +msgid "Static Files" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/contrib/syndication/apps.py:7 +msgid "Syndication" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/paginator.py:45 +msgid "That page number is not an integer" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/paginator.py:47 +msgid "That page number is less than 1" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/paginator.py:52 +msgid "That page contains no results" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:32 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid value." +msgstr "邮箱地址:" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:103 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:659 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid URL." +msgstr "邮箱地址:" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:155 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid integer." +msgstr "邮箱地址:" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:166 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid email address." +msgstr "邮箱地址:" + +#. Translators: "letters" means latin letters: a-z and A-Z. +#: venv3/lib/python3.8/site-packages/django/core/validators.py:240 +msgid "" +"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:247 +msgid "" +"Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " +"hyphens." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:256 +#: venv3/lib/python3.8/site-packages/django/core/validators.py:276 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid IPv4 address." +msgstr "邮箱地址:" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:261 +#: venv3/lib/python3.8/site-packages/django/core/validators.py:277 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid IPv6 address." +msgstr "邮箱地址:" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:271 +#: venv3/lib/python3.8/site-packages/django/core/validators.py:275 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid IPv4 or IPv6 address." +msgstr "邮箱地址:" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:305 +msgid "Enter only digits separated by commas." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:311 +#, python-format +msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:343 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:352 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:362 +#, python-format +msgid "" +"Ensure this value has at least %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at least %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:377 +#, python-format +msgid "" +"Ensure this value has at most %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at most %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:396 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:291 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:326 +#, fuzzy +#| msgid "Series number:" +msgid "Enter a number." +msgstr "系列编号:" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:398 +#, python-format +msgid "Ensure that there are no more than %(max)s digit in total." +msgid_plural "Ensure that there are no more than %(max)s digits in total." +msgstr[0] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:403 +#, python-format +msgid "Ensure that there are no more than %(max)s decimal place." +msgid_plural "Ensure that there are no more than %(max)s decimal places." +msgstr[0] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:408 +#, python-format +msgid "" +"Ensure that there are no more than %(max)s digit before the decimal point." +msgid_plural "" +"Ensure that there are no more than %(max)s digits before the decimal point." +msgstr[0] "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:470 +#, python-format +msgid "" +"File extension “%(extension)s” is not allowed. Allowed extensions are: " +"%(allowed_extensions)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/core/validators.py:522 +msgid "Null characters are not allowed." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/base.py:1181 +#: venv3/lib/python3.8/site-packages/django/forms/models.py:759 +msgid "and" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/base.py:1183 +#, fuzzy, python-format +#| msgid "A user with this email already exists." +msgid "%(model_name)s with this %(field_labels)s already exists." +msgstr "已经存在使用该邮箱的用户。" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:104 +#, fuzzy, python-format +#| msgid "%(value)s is not a valid remote_id" +msgid "Value %(value)r is not a valid choice." +msgstr "%(value)s 不是有效的 remote_id" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:105 +#, fuzzy +#| msgid "This shelf is empty." +msgid "This field cannot be null." +msgstr "此书架是空的。" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:106 +msgid "This field cannot be blank." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:107 +#, fuzzy, python-format +#| msgid "A user with this email already exists." +msgid "%(model_name)s with this %(field_label)s already exists." +msgstr "已经存在使用该邮箱的用户。" + +#. Translators: The 'lookup_type' is one of 'date', 'year' or 'month'. +#. Eg: "Title must be unique for pub_date year" +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:111 +#, python-format +msgid "" +"%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:130 +#, python-format +msgid "Field of type: %(field_type)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:937 +#, python-format +msgid "“%(value)s” value must be either True or False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:938 +#, python-format +msgid "“%(value)s” value must be either True, False, or None." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:940 +msgid "Boolean (Either True or False)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:981 +#, python-format +msgid "String (up to %(max_length)s)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1045 +#, fuzzy +#| msgid "No active invites" +msgid "Comma-separated integers" +msgstr "无有效的邀请" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1094 +#, python-format +msgid "" +"“%(value)s” value has an invalid date format. It must be in YYYY-MM-DD " +"format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1096 +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1239 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD) but it is an invalid " +"date." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1099 +msgid "Date (without time)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1237 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." +"uuuuuu]][TZ] format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1241 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" +"[TZ]) but it is an invalid date/time." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1245 +msgid "Date (with time)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1393 +#, python-format +msgid "“%(value)s” value must be a decimal number." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1395 +#, fuzzy +#| msgid "Series number:" +msgid "Decimal number" +msgstr "系列编号:" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1534 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in [DD] [[HH:]MM:]ss[." +"uuuuuu] format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1537 +#, fuzzy +#| msgid "List curation:" +msgid "Duration" +msgstr "列表策展:" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1587 +#, fuzzy +#| msgid "Email address:" +msgid "Email address" +msgstr "邮箱地址:" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1610 +msgid "File path" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1676 +#, python-format +msgid "“%(value)s” value must be a float." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1678 +msgid "Floating point number" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1716 +#, python-format +msgid "“%(value)s” value must be an integer." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1718 +msgid "Integer" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1801 +#, fuzzy +#| msgid "No active invites" +msgid "Big (8 byte) integer" +msgstr "无有效的邀请" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1817 +#, fuzzy +#| msgid "Email address:" +msgid "IPv4 address" +msgstr "邮箱地址:" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1848 +#, fuzzy +#| msgid "Email address:" +msgid "IP address" +msgstr "邮箱地址:" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1928 +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1929 +#, python-format +msgid "“%(value)s” value must be either None, True or False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1931 +msgid "Boolean (Either True, False or None)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1966 +#, fuzzy +#| msgid "No active invites" +msgid "Positive integer" +msgstr "无有效的邀请" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1979 +#, fuzzy +#| msgid "No active invites" +msgid "Positive small integer" +msgstr "无有效的邀请" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:1993 +#, python-format +msgid "Slug (up to %(max_length)s)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2025 +msgid "Small integer" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2032 +msgid "Text" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2060 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] " +"format." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2062 +#, python-format +msgid "" +"“%(value)s” value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an " +"invalid time." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2065 +msgid "Time" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2191 +msgid "URL" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2213 +msgid "Raw binary data" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2278 +#, fuzzy, python-format +#| msgid "%(value)s is not a valid username" +msgid "“%(value)s” is not a valid UUID." +msgstr "%(value)s 不是有效的用户名" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/__init__.py:2280 +msgid "Universally unique identifier" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/files.py:221 +msgid "File" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/files.py:360 +#, fuzzy +#| msgid "Images" +msgid "Image" +msgstr "图像" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:778 +#, python-format +msgid "%(model)s instance with %(field)s %(value)r does not exist." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:780 +msgid "Foreign Key (type determined by related field)" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1012 +#, fuzzy +#| msgid "Relationships" +msgid "One-to-one relationship" +msgstr "关系" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1066 +#, fuzzy, python-format +#| msgid "Relationships" +msgid "%(from)s-%(to)s relationship" +msgstr "关系" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1067 +#, fuzzy, python-format +#| msgid "Relationships" +msgid "%(from)s-%(to)s relationships" +msgstr "关系" + +#: venv3/lib/python3.8/site-packages/django/db/models/fields/related.py:1109 +#, fuzzy +#| msgid "Relationships" +msgid "Many-to-many relationship" +msgstr "关系" + +#. Translators: If found as last label character, these punctuation +#. characters will prevent the default label_suffix to be appended to the label +#: venv3/lib/python3.8/site-packages/django/forms/boundfield.py:149 +msgid ":?.!" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:53 +#, fuzzy +#| msgid "This shelf is empty." +msgid "This field is required." +msgstr "此书架是空的。" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:246 +#, fuzzy +#| msgid "Series number:" +msgid "Enter a whole number." +msgstr "系列编号:" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:397 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:1127 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid date." +msgstr "邮箱地址:" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:421 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:1128 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid time." +msgstr "邮箱地址:" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:443 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid date/time." +msgstr "邮箱地址:" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:472 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid duration." +msgstr "邮箱地址:" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:473 +#, python-brace-format +msgid "The number of days must be between {min_days} and {max_days}." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:533 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:534 +msgid "No file was submitted." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:535 +#, fuzzy +#| msgid "This shelf is empty." +msgid "The submitted file is empty." +msgstr "此书架是空的。" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:537 +#, python-format +msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." +msgid_plural "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr[0] "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:540 +msgid "Please either submit a file or check the clear checkbox, not both." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:601 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:763 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:853 +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1275 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:854 +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:969 +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1274 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a list of values." +msgstr "邮箱地址:" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:970 +msgid "Enter a complete value." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/fields.py:1186 +#, fuzzy +#| msgid "Email address:" +msgid "Enter a valid UUID." +msgstr "邮箱地址:" + +#. Translators: This is the default suffix added to form field labels +#: venv3/lib/python3.8/site-packages/django/forms/forms.py:81 +msgid ":" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/forms.py:207 +#, python-format +msgid "(Hidden field %(name)s) %(error)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:93 +msgid "ManagementForm data is missing or has been tampered with" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:345 +#, python-format +msgid "Please submit %d or fewer forms." +msgid_plural "Please submit %d or fewer forms." +msgstr[0] "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:352 +#, python-format +msgid "Please submit %d or more forms." +msgid_plural "Please submit %d or more forms." +msgstr[0] "" + +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:379 +#: venv3/lib/python3.8/site-packages/django/forms/formsets.py:386 +#, fuzzy +#| msgid "Order by" +msgid "Order" +msgstr "排列顺序" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:754 +#, python-format +msgid "Please correct the duplicate data for %(field)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:758 +#, python-format +msgid "Please correct the duplicate data for %(field)s, which must be unique." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:764 +#, python-format +msgid "" +"Please correct the duplicate data for %(field_name)s which must be unique " +"for the %(lookup)s in %(date_field)s." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:773 +msgid "Please correct the duplicate values below." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1094 +msgid "The inline value did not match the parent instance." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1161 +msgid "Select a valid choice. That choice is not one of the available choices." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/models.py:1277 +#, fuzzy, python-format +#| msgid "%(value)s is not a valid username" +msgid "“%(pk)s” is not a valid value." +msgstr "%(value)s 不是有效的用户名" + +#: venv3/lib/python3.8/site-packages/django/forms/utils.py:162 +#, python-format +msgid "" +"%(datetime)s couldn’t be interpreted in time zone %(current_timezone)s; it " +"may be ambiguous or it may not exist." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:395 +msgid "Clear" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:396 +#, fuzzy +#| msgid "Start reading" +msgid "Currently" +msgstr "开始阅读" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:397 +#, fuzzy +#| msgid "Change shelf" +msgid "Change" +msgstr "改变书架" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:709 +msgid "Unknown" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:710 +msgid "Yes" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/forms/widgets.py:711 +msgid "No" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:788 +msgid "yes,no,maybe" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:817 +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:834 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:836 +#, python-format +msgid "%s KB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:838 +#, python-format +msgid "%s MB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:840 +#, python-format +msgid "%s GB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:842 +#, python-format +msgid "%s TB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/template/defaultfilters.py:844 +#, python-format +msgid "%s PB" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:62 +msgid "p.m." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:63 +msgid "a.m." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:68 +msgid "PM" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:69 +msgid "AM" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:150 +msgid "midnight" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dateformat.py:152 +msgid "noon" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:295 +msgid "Monday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:296 +msgid "Tuesday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:297 +msgid "Wednesday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:298 +msgid "Thursday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:6 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:299 +msgid "Friday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:7 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:300 +msgid "Saturday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:7 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:301 +msgid "Sunday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Mon" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Tue" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Wed" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Thu" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:10 +msgid "Fri" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:11 +#, fuzzy +#| msgid "Status" +msgid "Sat" +msgstr "状态" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:11 +msgid "Sun" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:281 +msgid "January" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:282 +msgid "February" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:283 +#, fuzzy +#| msgid "Search" +msgid "March" +msgstr "搜索" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:284 +msgid "April" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:285 +msgid "May" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:14 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:286 +msgid "June" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:287 +msgid "July" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:288 +msgid "August" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:289 +#, fuzzy +#| msgid "Series number:" +msgid "September" +msgstr "系列编号:" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:290 +msgid "October" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:15 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:291 +msgid "November" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:16 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:292 +#, fuzzy +#| msgid "Series number:" +msgid "December" +msgstr "系列编号:" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "jan" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "feb" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "mar" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "apr" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "may" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:19 +msgid "jun" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "jul" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "aug" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "sep" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "oct" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "nov" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:20 +msgid "dec" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:23 +msgctxt "abbrev. month" +msgid "Jan." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:24 +msgctxt "abbrev. month" +msgid "Feb." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:25 +#, fuzzy +#| msgid "Search" +msgctxt "abbrev. month" +msgid "March" +msgstr "搜索" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:26 +msgctxt "abbrev. month" +msgid "April" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:27 +msgctxt "abbrev. month" +msgid "May" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:28 +msgctxt "abbrev. month" +msgid "June" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:29 +msgctxt "abbrev. month" +msgid "July" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:30 +msgctxt "abbrev. month" +msgid "Aug." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:31 +msgctxt "abbrev. month" +msgid "Sept." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:32 +msgctxt "abbrev. month" +msgid "Oct." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:33 +msgctxt "abbrev. month" +msgid "Nov." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:34 +msgctxt "abbrev. month" +msgid "Dec." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:37 +msgctxt "alt. month" +msgid "January" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:38 +msgctxt "alt. month" +msgid "February" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:39 +#, fuzzy +#| msgid "Search" +msgctxt "alt. month" +msgid "March" +msgstr "搜索" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:40 +msgctxt "alt. month" +msgid "April" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:41 +msgctxt "alt. month" +msgid "May" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:42 +msgctxt "alt. month" +msgid "June" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:43 +msgctxt "alt. month" +msgid "July" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:44 +msgctxt "alt. month" +msgid "August" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:45 +#, fuzzy +#| msgid "Series number:" +msgctxt "alt. month" +msgid "September" +msgstr "系列编号:" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:46 +msgctxt "alt. month" +msgid "October" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:47 +msgctxt "alt. month" +msgid "November" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/dates.py:48 +#, fuzzy +#| msgid "Series number:" +msgctxt "alt. month" +msgid "December" +msgstr "系列编号:" + +#: venv3/lib/python3.8/site-packages/django/utils/ipv6.py:8 +#, fuzzy +#| msgid "Email address:" +msgid "This is not a valid IPv6 address." +msgstr "邮箱地址:" + +#: venv3/lib/python3.8/site-packages/django/utils/text.py:69 +#, python-format +msgctxt "String to return when truncating text" +msgid "%(truncated_text)s…" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/text.py:235 +msgid "or" +msgstr "" + +#. Translators: This string is used as a separator between list elements +#: venv3/lib/python3.8/site-packages/django/utils/text.py:254 +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:83 +msgid ", " +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:9 +#, python-format +msgid "%d year" +msgid_plural "%d years" +msgstr[0] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:10 +#, python-format +msgid "%d month" +msgid_plural "%d months" +msgstr[0] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:11 +#, python-format +msgid "%d week" +msgid_plural "%d weeks" +msgstr[0] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:12 +#, python-format +msgid "%d day" +msgid_plural "%d days" +msgstr[0] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:13 +#, python-format +msgid "%d hour" +msgid_plural "%d hours" +msgstr[0] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:14 +#, python-format +msgid "%d minute" +msgid_plural "%d minutes" +msgstr[0] "" + +#: venv3/lib/python3.8/site-packages/django/utils/timesince.py:72 +msgid "0 minutes" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:110 +msgid "Forbidden" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:111 +msgid "CSRF verification failed. Request aborted." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:115 +msgid "" +"You are seeing this message because this HTTPS site requires a “Referer " +"header” to be sent by your Web browser, but none was sent. This header is " +"required for security reasons, to ensure that your browser is not being " +"hijacked by third parties." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:120 +msgid "" +"If you have configured your browser to disable “Referer” headers, please re-" +"enable them, at least for this site, or for HTTPS connections, or for “same-" +"origin” requests." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:124 +msgid "" +"If you are using the tag or " +"including the “Referrer-Policy: no-referrer” header, please remove them. The " +"CSRF protection requires the “Referer” header to do strict referer checking. " +"If you’re concerned about privacy, use alternatives like for links to third-party sites." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:132 +msgid "" +"You are seeing this message because this site requires a CSRF cookie when " +"submitting forms. This cookie is required for security reasons, to ensure " +"that your browser is not being hijacked by third parties." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:137 +msgid "" +"If you have configured your browser to disable cookies, please re-enable " +"them, at least for this site, or for “same-origin” requests." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/csrf.py:142 +msgid "More information is available with DEBUG=True." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:41 +msgid "No year specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:61 +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:111 +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:208 +msgid "Date out of range" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:90 +msgid "No month specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:142 +msgid "No day specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:188 +msgid "No week specified" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:338 +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:367 +#, python-format +msgid "No %(verbose_name_plural)s available" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:589 +#, python-format +msgid "" +"Future %(verbose_name_plural)s not available because %(class_name)s." +"allow_future is False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/dates.py:623 +#, python-format +msgid "Invalid date string “%(datestr)s” given format “%(format)s”" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/detail.py:54 +#, fuzzy, python-format +#| msgid "No books found matching the query \"%(query)s\"" +msgid "No %(verbose_name)s found matching the query" +msgstr "没有符合 \"%(query)s\" 请求的书目" + +#: venv3/lib/python3.8/site-packages/django/views/generic/list.py:67 +msgid "Page is not “last”, nor can it be converted to an int." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/list.py:72 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/generic/list.py:154 +#, python-format +msgid "Empty list and “%(class_name)s.allow_empty” is False." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/static.py:40 +msgid "Directory indexes are not allowed here." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/static.py:42 +#, python-format +msgid "“%(path)s” does not exist" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/static.py:80 +#, python-format +msgid "Index of %(directory)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:7 +msgid "Django: the Web framework for perfectionists with deadlines." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:346 +#, python-format +msgid "" +"View release notes for Django %(version)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:368 +msgid "The install worked successfully! Congratulations!" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:369 +#, python-format +msgid "" +"You are seeing this page because DEBUG=True is in your settings file and you have not configured any " +"URLs." +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:384 +msgid "Django Documentation" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:385 +msgid "Topics, references, & how-to’s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:396 +msgid "Tutorial: A Polling App" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:397 +msgid "Get started with Django" +msgstr "" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:408 +#, fuzzy +#| msgid "Community" +msgid "Django Community" +msgstr "社区" + +#: venv3/lib/python3.8/site-packages/django/views/templates/default_urlconf.html:409 +msgid "Connect, get help, or contribute" +msgstr "" + +#: venv3/lib/python3.8/site-packages/kombu/transport/qpid.py:1301 +#, python-format +msgid "Attempting to connect to qpid with SASL mechanism %s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/kombu/transport/qpid.py:1306 +#, python-format +msgid "Connected to qpid with SASL mechanism %s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/kombu/transport/qpid.py:1324 +#, python-format +msgid "Unable to connect to qpid with SASL mechanism %s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:371 +msgid "1 second ago" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:377 +msgid "1 minute ago" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:382 +msgid "1 hour ago" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:385 +#, python-format +msgid "%(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:387 +msgid "yesterday" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:387 +#, python-format +msgid "yesterday at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:389 +#, python-format +msgid "%(weekday)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:389 +#, python-format +msgid "%(weekday)s at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:392 +#: venv3/lib/python3.8/site-packages/tornado/locale.py:445 +#, python-format +msgid "%(month_name)s %(day)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:394 +#, python-format +msgid "%(month_name)s %(day)s at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:399 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:401 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s at %(time)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:439 +#, python-format +msgid "%(weekday)s, %(month_name)s %(day)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/locale.py:462 +#, python-format +msgid "%(commas)s and %(last)s" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:68 +msgctxt "law" +msgid "right" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:69 +msgctxt "good" +msgid "right" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:71 +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:74 +msgctxt "organization" +msgid "club" +msgstr "" + +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:76 +#: venv3/lib/python3.8/site-packages/tornado/test/locale_test.py:77 +msgctxt "stick" +msgid "club" +msgstr "" + +#~ msgid "Deactivate user" +#~ msgstr "停用用户" + +#~ msgid "Reactivate user" +#~ msgstr "启用用户" + +#~ msgid "Date" +#~ msgstr "日期" + +#, python-format +#~ msgid "" +#~ "replied to %(username)s's review" +#~ msgstr "" +#~ "回复了 %(username)s书评" + +#, python-format +#~ msgid "" +#~ "replied to %(username)s's comment" +#~ msgstr "" +#~ "恢复了 %(username)s评论" + +#, python-format +#~ msgid "" +#~ "replied to %(username)s's quote" +#~ msgstr "" +#~ "回复了 %(username)s引用" + +#~ msgid "Remove tag" +#~ msgstr "移除标签" + +#~ msgid "Add tag" +#~ msgstr "添加标签" + +#, python-format +#~ msgid "Books tagged \"%(tag.name)s\"" +#~ msgstr "标有 \"%(tag.name)s\" 标签的书" + +#, fuzzy +#~| msgid "Started" +#~ msgid "Getting Started" +#~ msgstr "已开始" + +#, fuzzy +#~| msgid "No users found for \"%(query)s\"" +#~ msgid "No users were found for \"%(query)s\"" +#~ msgstr "没有找到 \"%(query)s\" 的用户" + +#~ msgid "Tags" +#~ msgstr "标签" + +#~ msgid "Your lists" +#~ msgstr "你的列表" + +#~ msgid "See all %(size)s lists" +#~ msgstr "查看所有 %(size)s 个列表" + +#~ msgid "Recent Lists" +#~ msgstr "最近的列表" + +#~ msgid "Published" +#~ msgstr "已出版" + +#~ msgid "External links" +#~ msgstr "外部链接" + +#~ msgid "OpenLibrary" +#~ msgstr "OpenLibrary" + +#~ msgid "Change shelf" +#~ msgstr "更换书架" + +#~ msgid "Unshelve" +#~ msgstr "取下书架" + +#~ msgid "Your Shelves" +#~ msgstr "你的书架" + +#~ msgid "%(username)s: Shelves" +#~ msgstr "%(username)s: 书架" + +#~ msgid "Shelves" +#~ msgstr "书架" + +#~ msgid "See all %(shelf_count)s shelves" +#~ msgstr "查看所有 %(shelf_count)s 个书架" + +#~ msgid "Send follow request" +#~ msgstr "发送关注请求" + +#~ msgid "Announcements" +#~ msgstr "公告" + +#~ msgid "Site Configuration" +#~ msgstr "站点配置" + +#~ msgid "Follow request already sent." +#~ msgstr "已经发送关注请求。" + +#~ msgid "Created and curated by" +#~ msgstr "创建者及策展者为" + +#~ msgid "Created by" +#~ msgstr "创建者为" + +#~ msgid "Create New Shelf" +#~ msgstr "新建书架" + +#~ msgid "Create new list" +#~ msgstr "新建列表" + +#~ msgid "Added by" +#~ msgstr "添加来自" diff --git a/nginx/default.conf b/nginx/development similarity index 100% rename from nginx/default.conf rename to nginx/development diff --git a/nginx/production b/nginx/production new file mode 100644 index 00000000..c5d83cbf --- /dev/null +++ b/nginx/production @@ -0,0 +1,72 @@ +upstream web { + server web:8000; +} + +server { + listen [::]:80; + listen 80; + + server_name your-domain.com www.your-domain.com; + + location ~ /.well-known/acme-challenge { + allow all; + root /var/www/certbot; + } + +# # redirect http to https +# return 301 https://your-domain.com$request_uri; +# } +# +# server { +# listen [::]:443 ssl http2; +# listen 443 ssl http2; +# +# server_name your-domain.com; +# +# # SSL code +# ssl_certificate /etc/nginx/ssl/live/your-domain.com/fullchain.pem; +# ssl_certificate_key /etc/nginx/ssl/live/your-domain.com/privkey.pem; +# +# location ~ /.well-known/acme-challenge { +# allow all; +# root /var/www/certbot; +# } +# +# location / { +# proxy_pass http://web; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header Host $host; +# proxy_redirect off; +# } +# +# location /images/ { +# alias /app/images/; +# } +# +# location /static/ { +# alias /app/static/; +# } +} + +# Reverse-Proxy server +# server { +# listen [::]:8001; +# listen 8001; + +# server_name your-domain.com www.your-domain.com; + +# location / { +# proxy_pass http://web; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header Host $host; +# proxy_redirect off; +# } + +# location /images/ { +# alias /app/images/; +# } + +# location /static/ { +# alias /app/static/; +# } +# } diff --git a/package.json b/package.json index 8059255d..b7abe434 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,12 @@ { + "scripts": { + "watch:static": "yarn watch \"./bw-dev collectstatic\" bookwyrm/static/**" + }, "devDependencies": { "eslint": "^7.23.0", "stylelint": "^13.12.0", "stylelint-config-standard": "^21.0.0", - "stylelint-order": "^4.1.0" + "stylelint-order": "^4.1.0", + "watch": "^1.0.2" } } diff --git a/requirements.txt b/requirements.txt index 16561da5..289d6fe6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ celery==4.4.2 -Django==3.1.6 +Django==3.2.0 django-model-utils==4.0.0 environs==7.2.0 flower==0.9.4 @@ -15,7 +15,7 @@ django-rename-app==0.1.2 pytz>=2021.1 # Dev -black==20.8b1 +black==21.4b0 coverage==5.1 pytest-django==4.1.0 pytest==6.1.2 diff --git a/yarn.lock b/yarn.lock index de4e0107..c1a1c181 100644 --- a/yarn.lock +++ b/yarn.lock @@ -768,6 +768,13 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +exec-sh@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" + integrity sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw== + dependencies: + merge "^1.2.0" + execall@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/execall/-/execall-2.0.0.tgz#16a06b5fe5099df7d00be5d9c06eecded1663b45" @@ -1368,6 +1375,11 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +merge@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" + integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== + micromark@~2.11.0: version "2.11.4" resolved "https://registry.yarnpkg.com/micromark/-/micromark-2.11.4.tgz#d13436138eea826383e822449c9a5c50ee44665a" @@ -1405,7 +1417,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.2.5: +minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -2183,6 +2195,14 @@ vfile@^4.0.0: unist-util-stringify-position "^2.0.0" vfile-message "^2.0.0" +watch@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c" + integrity sha1-NApxe952Vyb6CqB9ch4BR6VR3ww= + dependencies: + exec-sh "^0.2.0" + minimist "^1.2.0" + which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
    {{ user.username }}{{ user.username }} {{ user.created_date }} {{ user.last_active_date }} {% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}