Merge branch 'main' into titles

This commit is contained in:
Mouse Reeve 2021-02-28 09:02:21 -08:00
commit c4116c93b7
104 changed files with 2651 additions and 693 deletions

View file

@ -20,22 +20,25 @@ You can request an invite to https://bookwyrm.social by [email](mailto:mousereev
## Contributing
There are many ways you can contribute to this project, regardless of your level of technical expertise.
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 contributons 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.
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 seet up a video call during BookWyrm ["office hours"](https://calendly.com/mouse-reeve/30min).
If you have questions about the project or contributing, you can set up a video call during BookWyrm ["office hours"](https://calendly.com/mouse-reeve/30min).
### Translation
Do you speak a language besides English? BookWyrm needs localization! If you're comfortable using git and want to get into the code, there are [instructions](#workin-with-translations-and-locale-files) on how to create and edit localization files. If you feel more comfortable working in a regular text editor and would prefer not to run the application, get in touch directly and we can figure out a system, like emailing a text file, that works best.
### 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).
## About BookWyrm
### What it is and isn't
BookWyrm is a platform for social reading! You can use it to track what you're reading, review books, and follow your friends. It isn't primarily meant for cataloguing or as a datasource for books, but it does do both of those things to some degree.
BookWyrm is a platform for social reading! You can use it to track what you're reading, review books, and follow your friends. It isn't primarily meant for cataloguing or as a data-source for books, but it does do both of those things to some degree.
### The role of federation
BookWyrm is built on [ActivityPub](http://activitypub.rocks/). With ActivityPub, it inter-operates with different instances of BookWyrm, and other ActivityPub compliant services, like Mastodon. This means you can run an instance for your book club, and still follow your friend who posts on a server devoted to 20th century Russian speculative fiction. It also means that your friend on mastodon can read and comment on a book review that you post on your BookWyrm instance.
@ -68,7 +71,7 @@ Since the project is still in its early stages, the features are growing every d
- Private, followers-only, and public privacy levels for posting, shelves, and lists
- Option for users to manually approve followers
- Allow blocking and flagging for moderation
### The Tech Stack
Web backend
- [Django](https://www.djangoproject.com/) web server
@ -76,12 +79,12 @@ Web backend
- [ActivityPub](http://activitypub.rocks/) federation
- [Celery](http://celeryproject.org/) task queuing
- [Redis](https://redis.io/) task backend
Front end
- Django templates
- [Bulma.io](https://bulma.io/) css framework
- Vanilla JavaScript, in moderation
Deployment
- [Docker](https://www.docker.com/) and docker-compose
- [Gunicorn](https://gunicorn.org/) web runner
@ -109,19 +112,46 @@ 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
```
### Workin with translations and locale files
Text in the html files are wrapped in translation tags (`{% trans %}` and `{% blocktrans %}`), and Django generates locale files for all the strings in which you can add translations for the text. You can find existing translations in the `locale/` directory.
The application's language is set by a request header sent by your browser to the application, so to change the language of the application, you can change the default language requested by your browser.
#### Adding a locale
To start translation into a language which is currently supported, run the django-admin `makemessages` command with the language code for the language you want to add (like `de` for German, or `en-gb` for British English):
``` bash
./bw-dev makemessages -l <language code>
```
#### Editing a locale
When you have a locale file, open the `django.po` in the directory for the language (for example, if you were adding German, `locale/de/LC_MESSAGES/django.po`. All the the text in the application will be shown in paired strings, with `msgid` as the original text, and `msgstr` as the translation (by default, this is set to an empty string, and will display the original text).
Add you translations to the `msgstr` strings, and when you're ready, compile the locale by running:
``` bash
./bw-dev compilemessages
```
You can add the `-l <language code>` to only compile one language. When you refresh the application, you should see your translations at work.
## Installing in Production
This project is still young and isn't, at the momoment, very stable, so please procede with caution when running 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 again Ubuntu 20.04)
- 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 indivudal changes to container config to enable things like SSL or regular backups.
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:
@ -171,3 +201,4 @@ There are three concepts in the book data model:
- `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.

View file

@ -1,121 +1,13 @@
''' handle reading a csv from goodreads '''
import csv
import logging
from bookwyrm.importer import Importer
from bookwyrm import models
from bookwyrm.models import ImportJob, ImportItem
from bookwyrm.tasks import app
# GoodReads is the default importer, thus Importer follows its structure. For a more complete example of overriding see librarything_import.py
logger = logging.getLogger(__name__)
class GoodreadsImporter(Importer):
service = 'GoodReads'
def create_job(user, csv_file, include_reviews, privacy):
''' check over a csv and creates a database entry for the job'''
job = ImportJob.objects.create(
user=user,
include_reviews=include_reviews,
privacy=privacy
)
for index, entry in enumerate(list(csv.DictReader(csv_file))):
if not all(x in entry for x in ('ISBN13', 'Title', 'Author')):
raise ValueError('Author, title, and isbn must be in data.')
ImportItem(job=job, index=index, data=entry).save()
return job
def create_retry_job(user, original_job, items):
''' retry items that didn't import '''
job = ImportJob.objects.create(
user=user,
include_reviews=original_job.include_reviews,
privacy=original_job.privacy,
retry=True
)
for item in items:
ImportItem(job=job, index=item.index, data=item.data).save()
return job
def start_import(job):
''' initalizes a csv import job '''
result = import_data.delay(job.id)
job.task_id = result.id
job.save()
@app.task
def import_data(job_id):
''' does the actual lookup work in a celery task '''
job = ImportJob.objects.get(id=job_id)
try:
for item in job.items.all():
try:
item.resolve()
except Exception as e:# pylint: disable=broad-except
logger.exception(e)
item.fail_reason = 'Error loading book'
item.save()
continue
if item.book:
item.save()
# shelves book and handles reviews
handle_imported_book(
job.user, item, job.include_reviews, job.privacy)
else:
item.fail_reason = 'Could not find a match for book'
item.save()
finally:
job.complete = True
job.save()
def handle_imported_book(user, item, include_reviews, privacy):
''' process a goodreads csv and then post about it '''
if isinstance(item.book, models.Work):
item.book = item.book.default_edition
if not item.book:
return
existing_shelf = models.ShelfBook.objects.filter(
book=item.book, user=user).exists()
# shelve the book if it hasn't been shelved already
if item.shelf and not existing_shelf:
desired_shelf = models.Shelf.objects.get(
identifier=item.shelf,
user=user
)
models.ShelfBook.objects.create(
book=item.book, shelf=desired_shelf, user=user)
for read in item.reads:
# check for an existing readthrough with the same dates
if models.ReadThrough.objects.filter(
user=user, book=item.book,
start_date=read.start_date,
finish_date=read.finish_date
).exists():
continue
read.book = item.book
read.user = user
read.save()
if include_reviews and (item.rating or item.review):
review_title = 'Review of {!r} on Goodreads'.format(
item.book.title,
) 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,
)
def parse_fields(self, data):
data.update({'import_source': self.service })
# add missing 'Date Started' field
data.update({'Date Started': None })
return data

135
bookwyrm/importer.py Normal file
View file

@ -0,0 +1,135 @@
''' handle reading a csv from an external service, defaults are from GoodReads '''
import csv
import logging
from bookwyrm import models
from bookwyrm.models import ImportJob, ImportItem
from bookwyrm.tasks import app
logger = logging.getLogger(__name__)
class Importer:
service = 'Unknown'
delimiter = ','
encoding = 'UTF-8'
mandatory_fields = ['Title', 'Author']
def create_job(self, user, csv_file, include_reviews, privacy):
''' check over a csv and creates a database entry for the job'''
job = ImportJob.objects.create(
user=user,
include_reviews=include_reviews,
privacy=privacy
)
for index, entry in enumerate(list(csv.DictReader(csv_file, delimiter=self.delimiter ))):
if not all(x in entry for x in self.mandatory_fields):
raise ValueError('Author and title must be in data.')
entry = self.parse_fields(entry)
self.save_item(job, index, entry)
return job
def save_item(self, job, index, data):
ImportItem(job=job, index=index, data=data).save()
def parse_fields(self, entry):
entry.update({'import_source': self.service })
return entry
def create_retry_job(self, user, original_job, items):
''' retry items that didn't import '''
job = ImportJob.objects.create(
user=user,
include_reviews=original_job.include_reviews,
privacy=original_job.privacy,
retry=True
)
for item in items:
self.save_item(job, item.index, item.data)
return job
def start_import(self, job):
''' initalizes a csv import job '''
result = import_data.delay(self.service, job.id)
job.task_id = result.id
job.save()
@app.task
def import_data(source, job_id):
''' does the actual lookup work in a celery task '''
job = ImportJob.objects.get(id=job_id)
try:
for item in job.items.all():
try:
item.resolve()
except Exception as e:# pylint: disable=broad-except
logger.exception(e)
item.fail_reason = 'Error loading book'
item.save()
continue
if item.book:
item.save()
# shelves book and handles reviews
handle_imported_book(source,
job.user, item, job.include_reviews, job.privacy)
else:
item.fail_reason = 'Could not find a match for book'
item.save()
finally:
job.complete = True
job.save()
def handle_imported_book(source, user, item, include_reviews, privacy):
''' process a csv and then post about it '''
if isinstance(item.book, models.Work):
item.book = item.book.default_edition
if not item.book:
return
existing_shelf = models.ShelfBook.objects.filter(
book=item.book, user=user).exists()
# shelve the book if it hasn't been shelved already
if item.shelf and not existing_shelf:
desired_shelf = models.Shelf.objects.get(
identifier=item.shelf,
user=user
)
models.ShelfBook.objects.create(
book=item.book, shelf=desired_shelf, user=user)
for read in item.reads:
# check for an existing readthrough with the same dates
if models.ReadThrough.objects.filter(
user=user, book=item.book,
start_date=read.start_date,
finish_date=read.finish_date
).exists():
continue
read.book = item.book
read.user = user
read.save()
if include_reviews and (item.rating or item.review):
review_title = 'Review of {!r} on {!r}'.format(
item.book.title,
source,
) if item.review else ''
# 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,
)

View file

@ -0,0 +1,42 @@
''' handle reading a csv from librarything '''
import csv
import re
import math
from bookwyrm import models
from bookwyrm.models import ImportItem
from bookwyrm.importer import Importer
class LibrarythingImporter(Importer):
service = 'LibraryThing'
delimiter = '\t'
encoding = 'ISO-8859-1'
# mandatory_fields : fields matching the book title and author
mandatory_fields = ['Title', 'Primary Author']
def parse_fields(self, initial):
data = {}
data['import_source'] = self.service
data['Book Id'] = initial['Book Id']
data['Title'] = initial['Title']
data['Author'] = initial['Primary Author']
data['ISBN13'] = initial['ISBN']
data['My Review'] = initial['Review']
if initial['Rating']:
data['My Rating'] = math.ceil(float(initial['Rating']))
else:
data['My Rating'] = ''
data['Date Added'] = re.sub('\[|\]', '', initial['Entry Date'])
data['Date Started'] = re.sub('\[|\]', '', initial['Date Started'])
data['Date Read'] = re.sub('\[|\]', '', initial['Date Read'])
data['Exclusive Shelf'] = None
if data['Date Read']:
data['Exclusive Shelf'] = "read"
elif data['Date Started']:
data['Exclusive Shelf'] = "reading"
else:
data['Exclusive Shelf'] = "to-read"
return data

View file

@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2021-02-27 19:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bookwyrm', '0045_auto_20210210_2114'),
]
operations = [
migrations.AddField(
model_name='sitesettings',
name='privacy_policy',
field=models.TextField(default='Add a privacy policy here.'),
),
]

View file

@ -97,8 +97,8 @@ class ImportItem(models.Model):
def get_book_from_title_author(self):
''' search by title and author '''
search_term = construct_search_term(
self.data['Title'],
self.data['Author']
self.title,
self.author
)
search_result = connector_manager.first_search_result(
search_term, min_confidence=0.999
@ -149,6 +149,14 @@ class ImportItem(models.Model):
dateutil.parser.parse(self.data['Date Added']))
return None
@property
def date_started(self):
''' when the book was started '''
if "Date Started" in self.data and self.data['Date Started']:
return timezone.make_aware(
dateutil.parser.parse(self.data['Date Started']))
return None
@property
def date_read(self):
''' the date a book was completed '''
@ -160,18 +168,24 @@ class ImportItem(models.Model):
@property
def reads(self):
''' formats a read through dataset for the book in this line '''
if (self.shelf == 'reading'
and self.date_added and not self.date_read):
return [ReadThrough(start_date=self.date_added)]
start_date = self.date_started
# Goodreads special case (no 'date started' field)
if ((self.shelf == 'reading' or (self.shelf == 'read' and self.date_read))
and self.date_added and not start_date):
start_date = self.date_added
if (start_date and start_date is not None and not self.date_read):
return [ReadThrough(start_date=start_date)]
if self.date_read:
return [ReadThrough(
start_date=self.date_added,
start_date=start_date,
finish_date=self.date_read,
)]
return []
def __repr__(self):
return "<GoodreadsItem {!r}>".format(self.data['Title'])
return "<{!r}Item {!r}>".format(self.data['import_source'], self.data['Title'])
def __str__(self):
return "{} by {}".format(self.data['Title'], self.data['Author'])

View file

@ -20,6 +20,8 @@ class SiteSettings(models.Model):
default='Contact an administrator to get an invite')
code_of_conduct = models.TextField(
default='Add a code of conduct here.')
privacy_policy = models.TextField(
default='Add a privacy policy here.')
allow_registration = models.BooleanField(default=True)
logo = models.ImageField(
upload_to='logos/', null=True, blank=True

View file

@ -1,9 +1,10 @@
''' bookwyrm settings and configuration '''
import os
from environs import Env
import requests
from django.utils.translation import gettext_lazy as _
env = Env()
DOMAIN = env('DOMAIN')
@ -27,6 +28,7 @@ EMAIL_USE_TLS = env('EMAIL_USE_TLS', True)
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale'),]
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
@ -58,6 +60,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
@ -135,6 +138,10 @@ AUTH_PASSWORD_VALIDATORS = [
# https://docs.djangoproject.com/en/2.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGES = [
('en-us', _('English')),
]
TIME_ZONE = 'UTC'

View file

@ -1,4 +1,5 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block content %}
<div class="block">
@ -9,8 +10,8 @@
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
<div class="column is-narrow">
<a href="{{ author.local_path }}/edit">
<span class="icon icon-pencil" title="Edit Author">
<span class="is-sr-only">Edit Author</span>
<span class="icon icon-pencil" title="{% trans 'Edit Author' %}">
<span class="is-sr-only">{% trans "Edit Author" %}</span>
</span>
</a>
</div>
@ -25,12 +26,12 @@
</p>
{% endif %}
{% if author.wikipedia_link %}
<p><a href="{{ author.wikipedia_link }}" rel=”noopener” target="_blank">Wikipedia</a></p>
<p><a href="{{ author.wikipedia_link }}" rel=”noopener” target="_blank">{% trans "Wikipedia" %}</a></p>
{% endif %}
</div>
<div class="block">
<h3 class="title is-4">Books by {{ author.name }}</h3>
<h3 class="title is-4">{% blocktrans with name=author.name %}Books by {{ name }}{% endblocktrans %}</h3>
{% include 'snippets/book_tiles.html' with books=books %}
</div>
{% endblock %}

View file

@ -1,4 +1,5 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
{% block content %}
@ -23,8 +24,8 @@
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
<div class="column is-narrow">
<a href="{{ book.id }}/edit">
<span class="icon icon-pencil" title="Edit Book">
<span class="is-sr-only">Edit Book</span>
<span class="icon icon-pencil" title="{% trans "Edit Book" %}">
<span class="is-sr-only">{% trans "Edit Book" %}</span>
</span>
</a>
</div>
@ -39,13 +40,13 @@
{% if request.user.is_authenticated and not book.cover %}
<div class="box p-2">
<h3 class="title is-6 mb-1">Add cover</h3>
<h3 class="title is-6 mb-1">{% trans "Add cover" %}</h3>
<form name="add-cover" method="POST" action="/upload-cover/{{ book.id }}" enctype="multipart/form-data">
{% csrf_token %}
<label class="label">
<input type="file" name="cover" accept="image/*" enctype="multipart/form-data" id="id_cover" required>
</label>
<button class="button is-small is-primary" type="submit">Add</button>
<button class="button is-small is-primary" type="submit">{% trans "Add" %}</button>
</form>
</div>
{% endif %}
@ -54,21 +55,21 @@
<dl>
{% if book.isbn_13 %}
<div class="is-flex is-justify-content-space-between">
<dt>ISBN:</dt>
<dt>{% trans "ISBN:" %}</dt>
<dd>{{ book.isbn_13 }}</dd>
</div>
{% endif %}
{% if book.oclc_number %}
<div class="is-flex is-justify-content-space-between">
<dt>OCLC Number:</dt>
<dt>{% trans "OCLC Number:" %}</dt>
<dd>{{ book.oclc_number }}</dd>
</div>
{% endif %}
{% if book.asin %}
<div class="is-flex is-justify-content-space-between">
<dt>ASIN:</dt>
<dt>{% trans "ASIN:" %}</dt>
<dd>{{ book.asin }}</dd>
</div>
{% endif %}
@ -80,7 +81,7 @@
</p>
{% if book.openlibrary_key %}
<p><a href="https://openlibrary.org/books/{{ book.openlibrary_key }}" target="_blank" rel="noopener">View on OpenLibrary</a></p>
<p><a href="https://openlibrary.org/books/{{ book.openlibrary_key }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a></p>
{% endif %}
</section>
</div>
@ -98,11 +99,11 @@
<form name="add-description" method="POST" action="/add-description/{{ book.id }}">
{% csrf_token %}
<p class="fields is-grouped">
<label class="label"for="id_description">Description:</label>
<label class="label"for="id_description">{% trans "Description:" %}</label>
<textarea name="description" cols="None" rows="None" class="textarea" id="id_description"></textarea>
</p>
<div class="field">
<button class="button is-primary" type="submit">Save</button>
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
{% include 'snippets/toggle/close_button.html' with text="Cancel" controls_text="add-description" controls_uid=book.id hide_inactive=True %}
</div>
</form>
@ -134,20 +135,20 @@
{% if request.user.is_authenticated %}
<section class="block">
<header class="columns">
<h2 class="column title is-5 mb-1">Your reading activity</h2>
<h2 class="column title is-5 mb-1">{% trans "Your reading activity" %}</h2>
<div class="column is-narrow">
{% include 'snippets/toggle/open_button.html' with text="Add read dates" icon="plus" class="is-small" controls_text="add-readthrough" %}
</div>
</header>
{% if not readthroughs.exists %}
<p>You don't have any reading activity for this book.</p>
<p>{% trans "You don't have any reading activity for this book." %}</p>
{% endif %}
<section class="hidden box" id="add-readthrough">
<form name="add-readthrough" action="/create-readthrough" method="post">
{% include 'snippets/readthrough_form.html' with readthrough=None %}
<div class="field is-grouped">
<div class="control">
<button class="button is-primary" type="submit">Create</button>
<button class="button is-primary" type="submit">{% trans "Create" %}</button>
</div>
<div class="control">
{% include 'snippets/toggle/close_button.html' with text="Cancel" controls_text="add-readthrough" %}
@ -168,11 +169,11 @@
<section class="block">
<form name="tag" action="/tag/" method="post">
<label for="tags" class="is-3">Tags</label>
<label for="tags" class="is-3">{% trans "Tags" %}</label>
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input id="tags" class="input" type="text" name="name">
<button class="button" type="submit">Add tag</button>
<button class="button" type="submit">{% trans "Add tag" %}</button>
</form>
</section>
{% endif %}
@ -189,7 +190,7 @@
<div class="column is-narrow">
{% if book.subjects %}
<section class="content block">
<h2 class="title is-5">Subjects</h2>
<h2 class="title is-5">{% trans "Subjects" %}</h2>
<ul>
{% for subject in book.subjects %}
<li>{{ subject }}</li>
@ -200,7 +201,7 @@
{% if book.subject_places %}
<section class="content block">
<h2 class="title is-5">Places</h2>
<h2 class="title is-5">{% trans "Places" %}</h2>
<ul>
{% for place in book.subject_placess %}
<li>{{ place }}</li>
@ -211,7 +212,7 @@
{% if lists.exists %}
<section class="content block">
<h2 class="title is-5">Lists</h2>
<h2 class="title is-5">{% trans "Lists" %}</h2>
<ul>
{% for list in lists %}
<li><a href="{{ list.local_path }}">{{ list.name }}</a></li>
@ -240,7 +241,7 @@
{% include 'snippets/username.html' with user=rating.user %}
</div>
<div class="field is-grouped mb-0">
<div>rated it</div>
<div>{% trans "rated it" %}</div>
{% include 'snippets/stars.html' with rating=rating.rating %}
</div>
<div>
@ -257,4 +258,3 @@
</div>
{% endblock %}

View file

@ -1,21 +1,36 @@
{% extends 'layout.html' %}
{% block content %}
{% extends 'discover/landing_layout.html' %}
{% load i18n %}
{% block panel %}
<header class="block has-text-centered">
<h1 class="title">{{ site.name }}</h1>
<h2 class="subtitle">{{ site.instance_tagline }}</h2>
</header>
<div class="block columns mt-4">
<nav class="menu column is-one-quarter">
<h2 class="menu-label">About {{ site.name }}</h2>
<ul class="menu-list">
<li>
<a href="#coc">{% trans "Code of Conduct" %}</a>
</li>
<li>
<a href="#privacy">{% trans "Privacy Policy" %}</a>
</li>
</ul>
</nav>
{% include 'discover/icons.html' %}
<div class="column content">
<div class="block" id="coc">
<h2 class="title">{% trans "Code of Conduct" %}</h2>
<div class="content">
{{ site.code_of_conduct | safe }}
</div>
</div>
<section class="block">
{% include 'snippets/about.html' %}
</section>
<hr aria-hidden="true">
<div class="block">
<h2 class="title">Code of Conduct</h2>
<div class="content">
{{ site.code_of_conduct | safe }}
<div class="block" id="privacy">
<h2 class="title">{% trans "Privacy Policy" %}</h2>
<div class="content">
{{ site.privacy_policy | safe }}
</div>
</div>
</div>
</div>

View file

@ -1,39 +1,9 @@
{% extends 'layout.html' %}
{% block content %}
<header class="block has-text-centered">
<h1 class="title">{{ site.name }}</h1>
<h2 class="subtitle">{{ site.instance_tagline }}</h2>
</header>
{% include 'discover/icons.html' %}
{% if not request.user.is_authenticated %}
<section class="tile is-ancestor">
<div class="tile is-7 is-parent">
<div class="tile is-child box">
{% include 'snippets/about.html' %}
</div>
</div>
<div class="tile is-5 is-parent">
<div class="tile is-child box has-background-primary-light content">
{% if site.allow_registration %}
<h2 class="title">Join {{ site.name }}</h2>
<form name="register" method="post" action="/register">
{% include 'snippets/register_form.html' %}
</form>
{% else %}
<h2 class="title">This instance is closed</h2>
<p>{{ site.registration_closed_text | safe}}</p>
{% endif %}
</div>
</div>
</section>
{% endif %}
{% extends 'discover/landing_layout.html' %}
{% load i18n %}
{% block panel %}
<div class="block is-hidden-tablet">
<h2 class="title has-text-centered">Recent Books</h2>
<h2 class="title has-text-centered">{% trans "Recent Books" %}</h2>
</div>
<section class="tile is-ancestor">

View file

@ -1,21 +0,0 @@
<section class="level is-mobile">
<div class="level-item has-text-centered">
<div>
<p class="title has-text-weight-normal"><span class="icon icon-graphic-paperplane"></span></p>
<p class="heading">Decentralized</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title has-text-weight-normal"><span class="icon icon-graphic-heart"></span></p>
<p class="heading">Friendly</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title has-text-weight-normal"><span class="icon icon-graphic-banknote"></span></p>
<p class="heading">Anti-Corporate</p>
</div>
</div>
</section>

View file

@ -0,0 +1,67 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block content %}
<header class="block has-text-centered">
<h1 class="title">{{ site.name }}</h1>
<h2 class="subtitle">{{ site.instance_tagline }}</h2>
</header>
<section class="level is-mobile">
<div class="level-item has-text-centered">
<div>
<p class="title has-text-weight-normal"><span class="icon icon-graphic-paperplane"></span></p>
<p class="heading">{% trans "Decentralized" %}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title has-text-weight-normal"><span class="icon icon-graphic-heart"></span></p>
<p class="heading">{% trans "Friendly" %}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title has-text-weight-normal"><span class="icon icon-graphic-banknote"></span></p>
<p class="heading">{% trans "Anti-Corporate" %}</p>
</div>
</div>
</section>
<section class="tile is-ancestor">
<div class="tile is-7 is-parent">
<div class="tile is-child box">
{% include 'snippets/about.html' %}
</div>
</div>
<div class="tile is-5 is-parent">
{% if not request.user.is_authenticated %}
<div class="tile is-child box has-background-primary-light content">
{% if site.allow_registration %}
<h2 class="title">{% blocktrans with name=site.name %}Join {{ name }}{% endblocktrans %}</h2>
<form name="register" method="post" action="/register">
{% include 'snippets/register_form.html' %}
</form>
{% else %}
<h2 class="title">{% trans "This instance is closed" %}</h2>
<p>{{ site.registration_closed_text | safe}}</p>
{% endif %}
</div>
{% else %}
<div class="tile is-child box has-background-white-bis">
<h2 class="title is-4">{% trans "Your Account" %}</h2>
{% include 'user/user_preview.html' with user=request.user %}
<div class="box content">
{% if request.user.summary %}
{{ request.user.summary | to_markdown | safe }}
{% endif %}
</div>
</div>
{% endif %}
</div>
</section>
{% block panel %}{% endblock %}
{% endblock %}

View file

@ -1,4 +1,5 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load humanize %}
{% block content %}
<header class="block">
@ -6,9 +7,9 @@
Edit "{{ author.name }}"
</h1>
<div>
<p>Added: {{ author.created_date | naturaltime }}</p>
<p>Updated: {{ author.updated_date | naturaltime }}</p>
<p>Last edited by: <a href="{{ author.last_edited_by.remote_id }}">{{ author.last_edited_by.display_name }}</a></p>
<p>{% trans "Added:" %} {{ author.created_date | naturaltime }}</p>
<p>{% trans "Updated:" %} {{ author.updated_date | naturaltime }}</p>
<p>{% trans "Last edited by:" %} <a href="{{ author.last_edited_by.remote_id }}">{{ author.last_edited_by.display_name }}</a></p>
</div>
</header>
@ -24,45 +25,45 @@
<div class="columns">
<div class="column">
<h2 class="title is-4">Metadata</h2>
<p><label class="label" for="id_name">Name:</label> {{ form.name }}</p>
<h2 class="title is-4">{% trans "Metadata" %}</h2>
<p><label class="label" for="id_name">{% trans "Name:" %}</label> {{ form.name }}</p>
{% for error in form.name.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_bio">Bio:</label> {{ form.bio }}</p>
<p><label class="label" for="id_bio">{% trans "Bio:" %}</label> {{ form.bio }}</p>
{% for error in form.bio.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_wikipedia_link">Wikipedia link:</label> {{ form.wikipedia_link }}</p>
<p><label class="label" for="id_wikipedia_link">{% trans "Wikipedia link:" %}</label> {{ form.wikipedia_link }}</p>
{% for error in form.wikipedia_link.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_born">Birth date:</label> {{ form.born }}</p>
<p><label class="label" for="id_born">{% trans "Birth date:" %}</label> {{ form.born }}</p>
{% for error in form.born.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_died">Death date:</label> {{ form.died }}</p>
<p><label class="label" for="id_died">{% trans "Death date:" %}</label> {{ form.died }}</p>
{% for error in form.died.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="column">
<h2 class="title is-4">Author Identifiers</h2>
<p><label class="label" for="id_openlibrary_key">Openlibrary key:</label> {{ form.openlibrary_key }}</p>
<h2 class="title is-4">{% trans "Author Identifiers" %}</h2>
<p><label class="label" for="id_openlibrary_key">{% trans "Openlibrary key:" %}</label> {{ form.openlibrary_key }}</p>
{% for error in form.openlibrary_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_librarything_key">Librarything key:</label> {{ form.librarything_key }}</p>
<p><label class="label" for="id_librarything_key">{% trans "Librarything key:" %}</label> {{ form.librarything_key }}</p>
{% for error in form.librarything_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_goodreads_key">Goodreads key:</label> {{ form.goodreads_key }}</p>
<p><label class="label" for="id_goodreads_key">{% trans "Goodreads key:" %}</label> {{ form.goodreads_key }}</p>
{% for error in form.goodreads_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
@ -71,10 +72,9 @@
</div>
<div class="block">
<button class="button is-primary" type="submit">Save</button>
<a class="button" href="/author/{{ author.id }}">Cancel</a>
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
<a class="button" href="/author/{{ author.id }}">{% trans "Cancel" %}</a>
</div>
</form>
{% endblock %}

View file

@ -1,4 +1,5 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load humanize %}
{% block content %}
<header class="block">
@ -6,9 +7,9 @@
Edit "{{ book.title }}"
</h1>
<div>
<p>Added: {{ book.created_date | naturaltime }}</p>
<p>Updated: {{ book.updated_date | naturaltime }}</p>
<p>Last edited by: <a href="{{ book.last_edited_by.remote_id }}">{{ book.last_edited_by.display_name }}</a></p>
<p>{% trans "Added:" %} {{ book.created_date | naturaltime }}</p>
<p>{% trans "Updated:" %} {{ book.updated_date | naturaltime }}</p>
<p>{% trans "Last edited by:" %} <a href="{{ book.last_edited_by.remote_id }}">{{ book.last_edited_by.display_name }}</a></p>
</div>
</header>
@ -23,32 +24,32 @@
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
<div class="columns">
<div class="column">
<h2 class="title is-4">Metadata</h2>
<p class="fields is-grouped"><label class="label" for="id_title">Title:</label> {{ form.title }} </p>
<h2 class="title is-4">{% trans "Metadata" %}</h2>
<p class="fields is-grouped"><label class="label" for="id_title">{% trans "Title:" %}</label> {{ form.title }} </p>
{% for error in form.title.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label" for="id_subtitle">Subtitle:</label> {{ form.subtitle }} </p>
<p class="fields is-grouped"><label class="label" for="id_subtitle">{% trans "Subtitle:" %}</label> {{ form.subtitle }} </p>
{% for error in form.subtitle.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label" for="id_description">Description:</label> {{ form.description }} </p>
<p class="fields is-grouped"><label class="label" for="id_description">{% trans "Description:" %}</label> {{ form.description }} </p>
{% for error in form.description.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label" for="id_series">Series:</label> {{ form.series }} </p>
<p class="fields is-grouped"><label class="label" for="id_series">{% trans "Series:" %}</label> {{ form.series }} </p>
{% for error in form.series.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label" for="id_series_number">Series number:</label> {{ form.series_number }} </p>
<p class="fields is-grouped"><label class="label" for="id_series_number">{% trans "Series number:" %}</label> {{ form.series_number }} </p>
{% for error in form.series_number.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label" for="id_first_published_date">First published date:</label> {{ form.first_published_date }} </p>
<p class="fields is-grouped"><label class="label" for="id_first_published_date">{% trans "First published date:" %}</label> {{ form.first_published_date }} </p>
{% for error in form.first_published_date.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label" for="id_published_date">Published date:</label> {{ form.published_date }} </p>
<p class="fields is-grouped"><label class="label" for="id_published_date">{% trans "Published date:" %}</label> {{ form.published_date }} </p>
{% for error in form.published_date.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
@ -61,7 +62,7 @@
</div>
<div class="column is-narrow">
<div class="block">
<h2 class="title is-4">Cover</h2>
<h2 class="title is-4">{% trans "Cover" %}</h2>
<p>{{ form.cover }}</p>
{% for error in form.cover.errors %}
<p class="help is-danger">{{ error | escape }}</p>
@ -71,8 +72,8 @@
</div>
<div class="block">
<h2 class="title is-4">Physical Properties</h2>
<p class="fields is-grouped"><label class="label" for="id_physical_format">Format:</label> {{ form.physical_format }} </p>
<h2 class="title is-4">{% trans "Physical Properties" %}</h2>
<p class="fields is-grouped"><label class="label" for="id_physical_format">{% trans "Format:" %}</label> {{ form.physical_format }} </p>
{% for error in form.physical_format.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
@ -80,31 +81,31 @@
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label" for="id_pages">Pages:</label> {{ form.pages }} </p>
<p class="fields is-grouped"><label class="label" for="id_pages">{% trans "Pages:" %}</label> {{ form.pages }} </p>
{% for error in form.pages.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<h2 class="title is-4">Book Identifiers</h2>
<p class="fields is-grouped"><label class="label" for="id_isbn_13">ISBN 13:</label> {{ form.isbn_13 }} </p>
<h2 class="title is-4">{% trans "Book Identifiers" %}</h2>
<p class="fields is-grouped"><label class="label" for="id_isbn_13">{% trans "ISBN 13:" %}</label> {{ form.isbn_13 }} </p>
{% for error in form.isbn_13.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label" for="id_isbn_10">ISBN 10:</label> {{ form.isbn_10 }} </p>
<p class="fields is-grouped"><label class="label" for="id_isbn_10">{% trans "ISBN 10:" %}</label> {{ form.isbn_10 }} </p>
{% for error in form.isbn_10.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label" for="id_openlibrary_key">Openlibrary key:</label> {{ form.openlibrary_key }} </p>
<p class="fields is-grouped"><label class="label" for="id_openlibrary_key">{% trans "Openlibrary key:" %}</label> {{ form.openlibrary_key }} </p>
{% for error in form.openlibrary_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label" for="id_librarything_key">OCLC Number:</label> {{ form.oclc_number }} </p>
<p class="fields is-grouped"><label class="label" for="id_librarything_key">{% trans "OCLC Number:" %}</label> {{ form.oclc_number }} </p>
{% for error in form.oclc_number.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="fields is-grouped"><label class="label" for="id_asin">ASIN:</label> {{ form.asin }} </p>
<p class="fields is-grouped"><label class="label" for="id_asin">{% trans "ASIN:" %}</label> {{ form.asin }} </p>
{% for error in form.ASIN.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
@ -113,8 +114,8 @@
</div>
<div class="block">
<button class="button is-primary" type="submit">Save</button>
<a class="button" href="/book/{{ book.id }}">Cancel</a>
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
<a class="button" href="/book/{{ book.id }}">{% trans "Cancel" %}</a>
</div>
</form>

View file

@ -1,8 +1,9 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block content %}
<div class="block">
<h1 class="title">Editions of <a href="/book/{{ work.id }}">"{{ work.title }}"</a></h1>
<h1 class="title">{% blocktrans with path=work.local_path work_title=work.title %}Editions of <a href="{{ work_path }}">"{{ work_title }}"</a>{% endblocktrans %}</h1>
{% include 'snippets/book_tiles.html' with books=editions %}
</div>

View file

@ -1,9 +1,10 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block content %}
<div class="block">
<h1 class="title">Server Error</h1>
<p>Something went wrong! Sorry about that.</p>
<h1 class="title">{% trans "Server Error" %}</h1>
<p>{% trans "Something went wrong! Sorry about that." %}</p>
</div>
{% endblock %}

View file

@ -1,9 +1,10 @@
{% extends 'feed/feed_layout.html' %}
{% load i18n %}
{% block panel %}
<header class="block">
<h1 class="title">Direct Messages{% if partner %} with {% include 'snippets/username.html' with user=partner %}{% endif %}</h1>
{% if partner %}<p class="subtitle"><a href="/direct-messages"><span class="icon icon-arrow-left" aria-hidden="true"></span> All messages</a></p>{% endif %}
{% if partner %}<p class="subtitle"><a href="/direct-messages"><span class="icon icon-arrow-left" aria-hidden="true"></span> {% trans "All messages" %}</a></p>{% endif %}
</header>
<div class="box">
@ -12,7 +13,7 @@
<section class="block">
{% if not activities %}
<p>You have no messages right now.</p>
<p>{% trans "You have no messages right now." %}</p>
{% endif %}
{% for activity in activities %}
<div class="block">

View file

@ -1,18 +1,19 @@
{% extends 'feed/feed_layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block panel %}
<h1 class="title">{{ tab | title }} Timeline</h1>
<h1 class="title">{% blocktrans with tab_title=tab|title %}{{ tab_title }} Timeline{% endblocktrans %}</h1>
<div class="tabs">
<ul>
<li class="{% if tab == 'home' %}is-active{% endif %}">
<a href="/#feed">Home</a>
<a href="/#feed">{% trans "Home" %}</a>
</li>
<li class="{% if tab == 'local' %}is-active{% endif %}">
<a href="/local#feed">Local</a>
<a href="/local#feed">{% trans "Local" %}</a>
</li>
<li class="{% if tab == 'federated' %}is-active{% endif %}">
<a href="/federated#feed">Federated</a>
<a href="/federated#feed">{% trans "Federated" %}</a>
</li>
</ul>
</div>
@ -20,7 +21,7 @@
{# announcements and system messages #}
{% if not goal and tab == 'home' %}
{% now 'Y' as year %}
<section class="block hidden" aria-title="Announcements" data-hide="hide-{{ year }}-reading-goal">
<section class="block hidden" aria-title="{% trans 'Announcements' %}" data-hide="hide-{{ year }}-reading-goal">
{% include 'snippets/goal_card.html' with year=year %}
<hr>
</section>
@ -28,7 +29,7 @@
{# activity feed #}
{% if not activities %}
<p>There aren't any activities right now! Try following a user to get started</p>
<p>{% trans "There aren't any activities right now! Try following a user to get started" %}</p>
{% endif %}
{% for activity in activities %}
<div class="block">

View file

@ -1,4 +1,5 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block title %}Updates{% endblock %}
{% block content %}
@ -6,9 +7,9 @@
<div class="columns">
{% if user.is_authenticated %}
<div class="column is-one-third">
<h2 class="title is-5">Your books</h2>
<h2 class="title is-5">{% trans "Your books" %}</h2>
{% if not suggested_books %}
<p>There are no books here right now! Try searching for a book to get started</p>
<p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p>
{% else %}
<div class="tabs is-small">
<ul role="tablist">
@ -65,7 +66,7 @@
{% if goal %}
<section class="section">
<div class="block">
<h3 class="title is-4">{{ goal.year }} Reading Goal</h3>
<h3 class="title is-4">{% blocktrans with yar=goal.year %}{{ year }} Reading Goal{% endblocktrans %}</h3>
{% include 'snippets/goal_progress.html' with goal=goal %}
</div>
</section>

View file

@ -1,9 +1,10 @@
{% extends 'feed/feed_layout.html' %}
{% load i18n %}
{% block panel %}
<header class="block">
<a href="/#feed" class="button" data-back>
<span class="icon icon-arrow-left" aira-hidden="true"></span>
<span>Back</span>
<span>{% trans "Back" %}</span>
</a>
</header>

View file

@ -1,9 +1,10 @@
{% extends 'user/user_layout.html' %}
{% load i18n %}
{% block header %}
<div class="columns is-mobile">
<div class="column">
<h1 class="title">{{ year }} Reading Progress</h1>
<h1 class="title">{% blocktrans %}{{ year }} Reading Progress{% endblocktrans %}</h1>
</div>
{% if is_self and goal %}
<div class="column is-narrow">
@ -25,7 +26,7 @@
</h2>
</header>
<section class="card-content content">
<p>Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.</p>
<p>{% blocktrans %}Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.{% endblocktrans %}</p>
{% include 'snippets/goal_form.html' with goal=goal year=year %}
</section>
@ -34,7 +35,7 @@
{% endif %}
{% if not goal and user != request.user %}
<p>{{ user.display_name }} hasn't set a reading goal for {{ year }}.</p>
<p>{% blocktrans with name=user.display_name %}{{ name }} hasn't set a reading goal for {{ year }}.{% endblocktrans %}</p>
{% endif %}
{% if goal %}

View file

@ -1,32 +1,48 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load humanize %}
{% block content %}
<div class="block">
<h1 class="title">Import Books from GoodReads</h1>
<h1 class="title">{% trans "Import Books" %}</h1>
<form name="import" action="/import" method="post" enctype="multipart/form-data">
{% csrf_token %}
<label class="label" for="source">
<p>{% trans "Data source" %}</p>
<div class="select {{ class }}">
<select name="source" id="source">
<option value="GoodReads" {% if current == 'GoodReads' %}selected{% endif %}>
GoodReads
</option>
<option value="LibraryThing" {% if current == 'LibraryThing' %}selected{% endif %}>
LibraryThing
</option>
</select>
</div>
</label>
<div class="field">
{{ import_form.as_p }}
</div>
<div class="field">
<label class="label">
<input type="checkbox" name="include_reviews" checked> Include reviews
<input type="checkbox" name="include_reviews" checked> {% trans "Include reviews" %}
</label>
</div>
<div class="field">
<label class="label">
<p>Privacy setting for imported reviews:</p>
<p>{% trans "Privacy setting for imported reviews:" %}</p>
{% include 'snippets/privacy_select.html' with no_label=True %}
</label>
</div>
<button class="button is-primary" type="submit">Import</button>
<button class="button is-primary" type="submit">{% trans "Import" %}</button>
</form>
</div>
<div class="content block">
<h2 class="title">Recent Imports</h2>
<h2 class="title">{% trans "Recent Imports" %}</h2>
{% if not jobs %}
<p>No recent imports</p>
<p>{% trans "No recent imports" %}</p>
{% endif %}
<ul>
{% for job in jobs %}

View file

@ -1,34 +1,35 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
{% block content %}
<div class="block">
<h1 class="title">Import Status</h1>
<h1 class="title">{% trans "Import Status" %}</h1>
<p>
Import started: {{ job.created_date | naturaltime }}
{% trans "Import started:" %} {{ job.created_date | naturaltime }}
</p>
{% if job.complete %}
<p>
Import completed: {{ task.date_done | naturaltime }}
{% trans "Import completed:" %} {{ task.date_done | naturaltime }}
</p>
{% elif task.failed %}
<div class="notification is-danger">TASK FAILED</div>
<div class="notification is-danger">{% trans "TASK FAILED" %}</div>
{% endif %}
</div>
<div class="block">
{% if not job.complete %}
Import still in progress.
{% trans "Import still in progress." %}
<p>
(Hit reload to update!)
{% trans "(Hit reload to update!)" %}
</p>
{% endif %}
</div>
{% if failed_items %}
<div class="block">
<h2 class="title is-4">Failed to load</h2>
<h2 class="title is-4">{% trans "Failed to load" %}</h2>
{% if not job.retry %}
<form name="retry" action="/import/{{ job.id }}" method="post">
{% csrf_token %}
@ -52,10 +53,10 @@
<div class="block pt-1 select-all">
<label class="label">
<input type="checkbox" class="checkbox">
Select all
{% trans "Select all" %}
</label>
</div>
<button class="button" type="submit">Retry items</button>
<button class="button" type="submit">{% trans "Retry items" %}</button>
{% else %}
<ul>
{% for item in failed_items %}
@ -77,17 +78,17 @@
{% endif %}
<div class="block">
<h2 class="title is-4">Successfully imported</h2>
<h2 class="title is-4">{% trans "Successfully imported" %}</h2>
<table class="table">
<tr>
<th>
Book
{% trans "Book" %}
</th>
<th>
Title
{% trans "Title" %}
</th>
<th>
Author
{% trans "Author" %}
</th>
<th>
</th>
@ -110,7 +111,7 @@
<td>
{% if item.book %}
<span class="icon icon-check">
<span class="is-sr-only">Imported</span>
<span class="is-sr-only">{% trans "Imported" %}</span>
</span>
{% endif %}
</td>

View file

@ -1,11 +1,12 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block content %}
<div class="columns">
<div class="column">
<div class="block">
{% if valid %}
<h1 class="title">Create an Account</h1>
<h1 class="title">{% trans "Create an Account" %}</h1>
<div>
<form name="register" method="post" action="/register">
<input type=hidden name="invite_code" value="{{ invite.code }}">
@ -14,8 +15,8 @@
</div>
{% else %}
<div class="content">
<h1 class="title">Permission Denied</h1>
<p>Sorry! This invite code is no longer valid.</p>
<h1 class="title">{% trans "Permission Denied" %}</h1>
<p>{% trans "Sorry! This invite code is no longer valid." %}</p>
</div>
{% endif %}
</div>

View file

@ -1,4 +1,5 @@
{% load bookwyrm_tags %}
{% load i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
@ -33,8 +34,8 @@
</div>
<div class="control">
<button class="button" type="submit">
<span class="icon icon-search" title="Search">
<span class="is-sr-only">search</span>
<span class="icon icon-search" title="{% trans 'Search' %}">
<span class="is-sr-only">{% trans "Search" %}</span>
</span>
</button>
</div>
@ -43,8 +44,8 @@
<div role="button" tabindex="0" class="navbar-burger pulldown-menu" data-controls="main-nav" aria-expanded="false">
<div class="navbar-item mt-3">
<div class="icon icon-dots-three-vertical" title="Main navigation menu">
<span class="is-sr-only">Main navigation menu</span>
<div class="icon icon-dots-three-vertical" title="{% trans 'Main navigation menu' %}">
<span class="is-sr-only">{% trans "Main navigation menu" %}</span>
</div>
</div>
</div>
@ -54,13 +55,13 @@
<div class="navbar-start">
{% if request.user.is_authenticated %}
<a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item">
Your shelves
{% trans "Your shelves" %}
</a>
<a href="/#feed" class="navbar-item">
Feed
{% trans "Feed" %}
</a>
<a href="{% url 'lists' %}" class="navbar-item">
Lists
{% trans "Lists" %}
</a>
{% endif %}
</div>
@ -121,8 +122,8 @@
<div class="navbar-item">
<a href="/notifications" class="tags has-addons">
<span class="tag is-medium">
<span class="icon icon-bell" title="Notifications">
<span class="is-sr-only">Notifications</span>
<span class="icon icon-bell" title="{% trans 'Notifications' %}">
<span class="is-sr-only">{% trans "Notifications" %}</span>
</span>
</span>
<span class="{% if not request.user|notification_count %}hidden {% endif %}tag is-danger is-medium" data-poll="notifications">
@ -139,16 +140,16 @@
{% csrf_token %}
<div class="columns is-variable is-1">
<div class="column">
<label class="is-sr-only" for="id_localname">Username:</label>
<label class="is-sr-only" for="id_localname">{% trans "Username:" %}</label>
<input type="text" name="localname" maxlength="150" class="input" required="" id="id_localname" placeholder="username">
</div>
<div class="column">
<label class="is-sr-only" for="id_password">Username:</label>
<label class="is-sr-only" for="id_password">{% trans "Username:" %}</label>
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password" placeholder="password">
<p class="help"><a href="/password-reset">Forgot your password?</a></p>
</div>
<div class="column is-narrow">
<button class="button is-primary" type="submit">Log in</button>
<button class="button is-primary" type="submit">{% trans "Log in" %}</button>
</div>
</div>
</form>
@ -179,11 +180,11 @@
<div class="columns">
<div class="column">
<p>
<a href="/about">About this server</a>
<a href="/about">{% trans "About this server" %}</a>
</p>
{% if site.admin_email %}
<p>
<a href="mailto:{{ site.admin_email }}">Contact site admin</a>
<a href="mailto:{{ site.admin_email }}">{% trans "Contact site admin" %}</a>
</p>
{% endif %}
</div>

View file

@ -1,7 +1,8 @@
{% extends 'components/inline_form.html' %}
{% load i18n %}
{% block header %}
Create List
{% trans "Create List" %}
{% endblock %}
{% block form %}

View file

@ -1,17 +1,18 @@
{% extends 'lists/list_layout.html' %}
{% load i18n %}
{% block panel %}
<section class="content block">
<h2>Pending Books</h2>
<p><a href="{% url 'list' list.id %}">Go to list</a></p>
<h2>{% trans "Pending Books" %}</h2>
<p><a href="{% url 'list' list.id %}">{% trans "Go to list" %}</a></p>
{% if not pending.exists %}
<p>You're all set!</p>
<p>{% trans "You're all set!" %}</p>
{% else %}
<table class="table is-striped">
<tr>
<th></th>
<th>Book</th>
<th>Suggested by</th>
<th>{% trans "Book" %}</th>
<th>{% trans "Suggested by" %}</th>
<th></th>
</tr>
{% for item in pending %}
@ -31,13 +32,13 @@
{% csrf_token %}
<input type="hidden" name="item" value="{{ item.id }}">
<input type="hidden" name="approved" value="true">
<button class="button">Approve</button>
<button class="button">{% trans "Approve" %}</button>
</form>
<form class="control" method="POST" action="{% url 'list-curate' list.id %}">
{% csrf_token %}
<input type="hidden" name="item" value="{{ item.id }}">
<input type="hidden" name="approved" value="false">
<button class="button is-danger is-light">Discard</button>
<button class="button is-danger is-light">{% trans "Discard" %}</button>
</div>
</form>
</td>

View file

@ -1,7 +1,8 @@
{% extends 'components/inline_form.html' %}
{% load i18n %}
{% block header %}
Edit List
{% trans "Edit List" %}
{% endblock %}
{% block form %}

View file

@ -1,34 +1,35 @@
{% load i18n %}
{% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}">
<div class="columns">
<div class="column">
<div class="field">
<label class="label" for="id_name">Name:</label>
<label class="label" for="id_name">{% trans "Name:" %}</label>
{{ list_form.name }}
</div>
<div class="field">
<label class="label" for="id_description">Description:</label>
<label class="label" for="id_description">{% trans "Description:" %}</label>
{{ list_form.description }}
</div>
</div>
<div class="column">
<fieldset class="field">
<legend class="label">List curation:</legend>
<legend class="label">{% trans "List curation:" %}</legend>
<label class="field">
<input type="radio" name="curation" value="closed"{% if not list or list.curation == 'closed' %} checked{% endif %}> Closed
<p class="help mb-2">Only you can add and remove books to this list</p>
<input type="radio" name="curation" value="closed"{% if not list or list.curation == 'closed' %} checked{% endif %}> {% trans "Closed" %}
<p class="help mb-2">{% trans "Only you can add and remove books to this list" %}</p>
</label>
<label class="field">
<input type="radio" name="curation" value="curated"{% if list.curation == 'curated' %} checked{% endif %}> Curated
<p class="help mb-2">Anyone can suggest books, subject to your approval</p>
<input type="radio" name="curation" value="curated"{% if list.curation == 'curated' %} checked{% endif %}> {% trans "Curated" %}
<p class="help mb-2">{% trans "Anyone can suggest books, subject to your approval" %}</p>
</label>
<label class="field">
<input type="radio" name="curation" value="open"{% if list.curation == 'open' %} checked{% endif %}> Open
<p class="help mb-2">Anyone can add books to this list</p>
<input type="radio" name="curation" value="open"{% if list.curation == 'open' %} checked{% endif %}> {% trans "Open" %}
<p class="help mb-2">{% trans "Anyone can add books to this list" %}</p>
</label>
</fieldset>
</div>
@ -38,7 +39,7 @@
{% include 'snippets/privacy_select.html' with current=list.privacy %}
</div>
<div class="control">
<button type="submit" class="button is-primary">Save</button>
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
</div>
</div>

View file

@ -1,4 +1,5 @@
{% extends 'lists/list_layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block panel %}
@ -13,7 +14,7 @@
<div class="columns mt-3">
<section class="column is-three-quarters">
{% if not items.exists %}
<p>This list is currently empty</p>
<p>{% trans "This list is currently empty" %}</p>
{% else %}
<ol>
{% for item in items %}
@ -31,13 +32,13 @@
</div>
<div class="card-footer has-background-white-bis">
<div class="card-footer-item">
<p>Added by {% include 'snippets/username.html' with user=item.user %}</p>
<p>{% trans "Added by" %} {% include 'snippets/username.html' with user=item.user %}</p>
</div>
{% if list.user == request.user or list.curation == 'open' and item.user == request.user %}
<form name="add-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item">
{% csrf_token %}
<input type="hidden" name="item" value="{{ item.id }}">
<button type="submit" class="button is-small is-danger">Remove</button>
<button type="submit" class="button is-small is-danger">{% trans "Remove" %}</button>
</form>
{% endif %}
</div>
@ -50,26 +51,29 @@
{% if request.user.is_authenticated and not list.curation == 'closed' or request.user == list.user %}
<section class="column is-one-quarter content">
<h2>{% if list.curation == 'open' or request.user == list.user %}Add{% else %}Suggest{% endif %} Books</h2>
<h2>{% if list.curation == 'open' or request.user == list.user %}{% trans "Add Books" %}{% else %}{% trans "Suggest Books" %}{% endif %}</h2>
<form name="search" action="{% url 'list' list.id %}" method="GET" class="block">
<div class="field has-addons">
<div class="control">
<input aria-label="Search for a book" class="input" type="text" name="q" placeholder="Search for a book" value="{{ query }}">
<input aria-label="{% trans 'Search for a book' %}" class="input" type="text" name="q" placeholder="{% trans 'Search for a book' %}" value="{{ query }}">
</div>
<div class="control">
<button class="button" type="submit">
<span class="icon icon-search" title="Search">
<span class="is-sr-only">search</span>
<span class="icon icon-search" title="{% trans 'Search' %}">
<span class="is-sr-only">{% trans "search" %}</span>
</span>
</button>
</div>
</div>
{% if query %}
<p class="help"><a href="{% url 'list' list.id %}">Clear search</a></p>
<p class="help"><a href="{% url 'list' list.id %}">{% trans "Clear search" %}</a></p>
{% endif %}
</form>
{% if not suggested_books %}
<p>No books found{% if query %} matching the query "{{ query }}"{% endif %}</p>
{% if query %}
<p>{% blocktrans %}No books found matching the query "{{ query }}"{% endblocktrans %}</p>{% else %}
<p>{% trans "No books found" %}</p>
{% endif %}
{% endif %}
{% for book in suggested_books %}
{% if book %}
@ -82,7 +86,7 @@
<form name="add-book" method="post" action="{% url 'list-add-book' list.id %}">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<button type="submit" class="button is-small is-link">{% if list.curation == 'open' or request.user == list.user %}Add{% else %}Suggest{% endif %}</button>
<button type="submit" class="button is-small is-link">{% if list.curation == 'open' or request.user == list.user %}{% trans "Add" %}{% else %}{% trans "Suggest" %}{% endif %}</button>
</form>
</div>
</div>

View file

@ -1,4 +1,5 @@
{% load bookwyrm_tags %}
{% load i18n %}
<div class="columns is-multiline">
{% for list in lists %}
<div class="column is-one-quarter">
@ -15,7 +16,7 @@
</div>
<div class="card-content is-flex-grow-0">
{% if list.description %}{{ list.description | to_markdown | safe | truncatewords_html:20 }}{% endif %}
<p class="subtitle help">Created {% if list.curation != 'open' %} and curated{% endif %} by {% include 'snippets/username.html' with user=list.user %}</p>
<p class="subtitle help">{% if list.curation != 'open' %}{% trans "Created and curated by" %}{% else %}{% trans "Created by" %}{% endif %} {% include 'snippets/username.html' with user=list.user %}</p>
</div>
</div>
</div>

View file

@ -1,11 +1,13 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block content %}
<header class="columns content is-mobile">
<div class="column">
<h1 class="title">{{ list.name }} <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span></h1>
<p class="subtitle help">Created {% if list.curation != 'open' %} and curated{% endif %} by {% include 'snippets/username.html' with user=list.user %}</p>
<p class="subtitle help">{% if list.curation != 'open' %}{% trans "Created and curated by" %}{% else %}{% trans "Created by" %} {% include 'snippets/username.html' with user=list.user %}</p>
{% endif %}
{% include 'snippets/trimmed_text.html' with full=list.description %}
</div>
{% if request.user == list.user %}

View file

@ -1,13 +1,14 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block content %}
<header class="block">
<h1 class="title">Lists</h1>
<h1 class="title">{% trans "Lists" %}</h1>
</header>
{% if request.user.is_authenticated and not lists.has_previous %}
<header class="block columns is-mobile">
<div class="column">
<h2 class="title">Your lists</h2>
<h2 class="title">{% trans "Your lists" %}</h2>
</div>
<div class="column is-narrow">
{% include 'snippets/toggle/open_button.html' with controls_text="create-list" icon="plus" text="Create new list" focus="create-list-header" %}
@ -32,7 +33,7 @@
{% if lists %}
<section class="block content">
<h2 class="title">Recent Lists</h2>
<h2 class="title">{% trans "Recent Lists" %}</h2>
{% include 'lists/list_items.html' with lists=lists %}
</section>
<div>

View file

@ -1,23 +1,24 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block content %}
<div class="columns">
<div class="column">
<div class="box">
<h1 class="title">Log in</h1>
<h1 class="title">{% trans "Log in" %}</h1>
{% if login_form.non_field_errors %}
<p class="notification is-danger">{{ login_form.non_field_errors }}</p>
{% endif %}
<form name="login" method="post" action="/login">
{% csrf_token %}
<div class="field">
<label class="label" for="id_localname">Username:</label>
<label class="label" for="id_localname">{% trans "Username:" %}</label>
<div class="control">
{{ login_form.localname }}
</div>
</div>
<div class="field">
<label class="label" for="id_password">Password:</label>
<label class="label" for="id_password">{% trans "Password:" %}</label>
<div class="control">
{{ login_form.password }}
</div>
@ -27,23 +28,23 @@
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-primary" type="submit">Log in</button>
<button class="button is-primary" type="submit">{% trans "Log in" %}</button>
</div>
<div class="control">
<small><a href="/password-reset">Forgot your password?</a></small>
<small><a href="/password-reset">{% trans "Forgot your password?" %}</a></small>
</div>
</div>
</form>
</div>
<div class="box has-background-primary-light">
{% if site.allow_registration %}
<h2 class="title">Create an Account</h2>
<h2 class="title">{% trans "Create an Account" %}</h2>
<form name="register" method="post" action="/register">
{% include 'snippets/register_form.html' %}
</form>
{% else %}
<h2 class="title">This instance is closed</h2>
<p>Contact an administrator to get an invite</p>
<h2 class="title">{% trans "This instance is closed" %}</h2>
<p>{% trans "Contact an administrator to get an invite" %}</p>
{% endif %}
</div>
</div>
@ -53,7 +54,7 @@
{% include 'snippets/about.html' %}
<p class="block">
<a href="/about/">More about this site</a>
<a href="/about/">{% trans "More about this site" %}</a>
</p>
</div>
</div>

View file

@ -1,9 +1,10 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block content %}
<div class="block">
<h1 class="title">Not Found</h1>
<p>The page your requested doesn't seem to exist!</p>
<h1 class="title">{% trans "Not Found" %}</h1>
<p>{% trans "The page your requested doesn't seem to exist!" %}</p>
</div>
{% endblock %}

View file

@ -1,13 +1,14 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load humanize %}
{% load bookwyrm_tags %}
{% block content %}
<div class="block">
<h1 class="title">Notifications</h1>
<h1 class="title">{% trans "Notifications" %}</h1>
<form name="clear" action="/notifications" method="POST">
{% csrf_token %}
<button class="button is-danger is-light" type="submit" class="secondary">Delete notifications</button>
<button class="button is-danger is-light" type="submit" class="secondary">{% trans "Delete notifications" %}</button>
</form>
</div>
@ -41,32 +42,29 @@
{% include 'snippets/avatar.html' with user=notification.related_user %}
{% include 'snippets/username.html' with user=notification.related_user %}
{% if notification.notification_type == 'FAVORITE' %}
favorited your
<a href="{{ related_status.local_path }}">{{ related_status | status_preview_name|safe }}</a>
{% blocktrans with preview_name=related_status|status_preview_name|safe related_path=related_status.local_path %}favorited your <a href="{{ related_path }}">{{ preview_name }}</a>{% endblocktrans %}
{% elif notification.notification_type == 'MENTION' %}
mentioned you in a
<a href="{{ related_status.local_path }}">{{ related_status | status_preview_name|safe }}</a>
{% blocktrans with preview_name=related_status|status_preview_name|safe related_path=related_status.local_path %}mentioned you in a <a href="{{ related_path }}">{{ preview_name }}</a>{% endblocktrans %}
{% elif notification.notification_type == 'REPLY' %}
<a href="{{ related_status.local_path }}">replied</a>
to your
<a href="{{ related_status.reply_parent.local_path }}">{{ related_status | status_preview_name|safe }}</a>
{% blocktrans with preview_name=related_status|status_preview_name|safe related_path=related_status.local_path parent_path=related_status.reply_parent.local_path %}<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">{{ preview_name }}</a>{% endblocktrans %}
{% elif notification.notification_type == 'FOLLOW' %}
followed you
{% trans "followed you" %}
{% include 'snippets/follow_button.html' with user=notification.related_user %}
{% elif notification.notification_type == 'FOLLOW_REQUEST' %}
sent you a follow request
{% trans "sent you a follow request" %}
<div class="row shrink">
{% include 'snippets/follow_request_buttons.html' with user=notification.related_user %}
</div>
{% elif notification.notification_type == 'BOOST' %}
boosted your <a href="{{ related_status.local_path }}">{{ related_status | status_preview_name|safe }}</a>
{% blocktrans with preview_name=related_status|status_preview_name|safe related_path=related_status.local_path %}boosted your <a href="{{ related_path }}">{{ preview_name }}</a>{% endblocktrans %}
{% elif notification.notification_type == 'ADD' %}
{% if notification.related_list_item.approved %}added{% else %}suggested adding{% endif %} {% include 'snippets/book_titleby.html' with book=notification.related_list_item.book %} to your list "<a href="{{ notification.related_list_item.book_list.local_path }}{% if not notification.related_list_item.approved %}/curate{% endif %}">{{ notification.related_list_item.book_list.name }}</a>"
{% if notification.related_list_item.approved %}{% trans "added" %}{% else %}{% trans "suggested adding" %}{% endif %} {% include 'snippets/book_titleby.html' with book=notification.related_list_item.book %} to your list "<a href="{{ notification.related_list_item.book_list.local_path }}{% if not notification.related_list_item.approved %}/curate{% endif %}">{{ notification.related_list_item.book_list.name }}</a>"
{% endif %}
{% elif notification.related_import %}
your <a href="/import/{{ notification.related_import.id }}">import</a> completed.
{% blocktrans with related_id=notification.related_import.id %} your <a href="/import/{{ related_id }}">import</a> completed.{% endblocktrans %}
{% endif %}
</p>
</div>
@ -98,7 +96,7 @@
{% endfor %}
{% if not notifications %}
<p>You're all caught up!</p>
<p>{% trans "You're all caught up!" %}</p>
{% endif %}
</div>
{% endblock %}

View file

@ -1,30 +1,31 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block content %}
<div class="columns">
<div class="column">
<div class="block">
<h1 class="title">Reset Password</h1>
<h1 class="title">{% trans "Reset Password" %}</h1>
{% for error in errors %}
<p class="is-danger">{{ error }}</p>
{% endfor %}
<form name="password-reset" method="post" action="/password-reset/{{ code }}">
{% csrf_token %}
<div class="field">
<label class="label" for="id_password">Password:</label>
<label class="label" for="id_password">{% trans "Password:" %}</label>
<div class="control">
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password">
</div>
</div>
<div class="field">
<label class="label" for="id_confirm_password">Confirm password:</label>
<label class="label" for="id_confirm_password">{% trans "Confirm password:" %}</label>
<div class="control">
<input type="password" name="confirm-password" maxlength="128" class="input" required="" id="id_confirm_password">
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-primary" type="submit">Confirm</button>
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button>
</div>
</div>
</form>
@ -36,7 +37,6 @@
{% include 'snippets/about.html' %}
</div>
</div>
</div>
{% endblock %}

View file

@ -1,23 +1,24 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block content %}
<div class="columns is-centered">
<div class="column is-half">
<div class="block">
<h1 class="title">Reset Password</h1>
<h1 class="title">{% trans "Reset Password" %}</h1>
{% if message %}<p>{{ message }}</p>{% endif %}
<p>A link to reset your password will be sent to your email address</p>
<p>{% trans "A link to reset your password will be sent to your email address" %}</p>
<form name="password-reset" method="post" action="/password-reset">
{% csrf_token %}
<div class="field">
<label class="label" for="id_email_register">Email address:</label>
<label class="label" for="id_email_register">{% trans "Email address:" %}</label>
<div class="control">
<input type="email" name="email" maxlength="254" class="input" id="id_email_register">
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-link" type="submit">Reset password</button>
<button class="button is-link" type="submit">{% trans "Reset password" %}</button>
</div>
</div>
</form>

View file

@ -1,12 +1,13 @@
{% extends 'preferences/preferences_layout.html' %}
{% load i18n %}
{% block header %}
Blocked Users
{% trans "Blocked Users" %}
{% endblock %}
{% block panel %}
{% if not request.user.blocks.exists %}
<p>No users currently blocked.</p>
<p>{% trans "No users currently blocked." %}</p>
{% else %}
<ul>
{% for user in request.user.blocks.all %}

View file

@ -1,19 +1,20 @@
{% extends 'preferences/preferences_layout.html' %}
{% load i18n %}
{% block header %}
Change Password
{% trans "Change Password" %}
{% endblock %}
{% block panel %}
<form name="edit-profile" action="/change-password/" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="block">
<label class="label" for="id_password">New password:</label>
<label class="label" for="id_password">{% trans "New password:" %}</label>
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password">
</div>
<div class="block">
<label class="label" for="id_confirm_password">Confirm password:</label>
<label class="label" for="id_confirm_password">{% trans "Confirm password:" %}</label>
<input type="password" name="confirm-password" maxlength="128" class="input" required="" id="id_confirm_password">
</div>
<button class="button is-primary" type="submit">Change password</button>
<button class="button is-primary" type="submit">{% trans "Change password" %}</button>
</form>
{% endblock %}

View file

@ -1,6 +1,7 @@
{% extends 'preferences/preferences_layout.html' %}
{% load i18n %}
{% block header %}
Edit Profile
{% trans "Edit Profile" %}
{% endblock %}
{% block panel %}
@ -10,28 +11,28 @@ Edit Profile
<form name="edit-profile" action="{% url 'prefs-profile' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="block">
<label class="label" for="id_avatar">Avatar:</label>
<label class="label" for="id_avatar">{% trans "Avatar:" %}</label>
{{ form.avatar }}
{% for error in form.avatar.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<label class="label" for="id_name">Display name:</label>
<label class="label" for="id_name">{% trans "Display name:" %}</label>
{{ form.name }}
{% for error in form.name.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<label class="label" for="id_summary">Summary:</label>
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
{{ form.summary }}
{% for error in form.summary.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<label class="label" for="id_email">Email address:</label>
<label class="label" for="id_email">{% trans "Email address:" %}</label>
{{ form.email }}
{% for error in form.email.errors %}
<p class="help is-danger">{{ error | escape }}</p>
@ -39,10 +40,10 @@ Edit Profile
</div>
<div class="block">
<label class="checkbox label" for="id_manually_approves_followers">
Manually approve followers:
{% trans "Manually approve followers:" %}
{{ form.manually_approves_followers }}
</label>
</div>
<button class="button is-primary" type="submit">Save</button>
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
</form>
{% endblock %}

View file

@ -1,4 +1,5 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block content %}
<header class="block column is-offset-one-quarter pl-1">
@ -7,19 +8,19 @@
<div class="block columns">
<nav class="menu column is-one-quarter">
<h2 class="menu-label">Account</h2>
<h2 class="menu-label">{% trans "Account" %}</h2>
<ul class="menu-list">
<li>
<a href="/preferences/profile"{% if '/preferences/profile' in request.path %} class="is-active" aria-selected="true"{% endif %}>Profile</a>
<a href="/preferences/profile"{% if '/preferences/profile' in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Profile" %}</a>
</li>
<li>
<a href="/preferences/password"{% if '/preferences/password' in request.path %} class="is-active" aria-selected="true"{% endif %}>Change password</a>
<a href="/preferences/password"{% if '/preferences/password' in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Change password" %}</a>
</li>
</ul>
<h2 class="menu-label">Relationships</h2>
<h2 class="menu-label">{% trans "Relationships" %}</h2>
<ul class="menu-list">
<li>
<a href="/preferences/block"{% if '/preferences/block' in request.path %} class="is-active" aria-selected="true"{% endif %}>Blocked users</a>
<a href="/preferences/block"{% if '/preferences/block' in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Blocked users" %}</a>
</li>
</ul>
</nav>

View file

@ -1,16 +1,17 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block content %}
{% with book_results|first as local_results %}
<div class="block">
<h1 class="title">Search Results for "{{ query }}"</h1>
<h1 class="title">{% blocktrans %}Search Results for "{{ query }}"{% endblocktrans %}</h1>
</div>
<div class="block columns">
<div class="column">
<h2 class="title">Matching Books</h2>
<h2 class="title">{% trans "Matching Books" %}</h2>
<section class="block">
{% if not local_results.results %}
<p>No books found for "{{ query }}"</p>
<p>{% blocktrans %}No books found for "{{ query }}"{% endblocktrans %}</p>
{% else %}
<ul>
{% for result in local_results.results %}
@ -26,7 +27,7 @@
{% if book_results|slice:":1" and local_results.results %}
<div class="block">
<p>
Didn't find what you were looking for?
{% trans "Didn't find what you were looking for?" %}
</p>
{% include 'snippets/toggle/open_button.html' with text="Show results from other catalogues" small=True controls_text="more-results" %}
</div>
@ -49,7 +50,7 @@
{% csrf_token %}
<input type="hidden" name="remote_id" value="{{ result.key }}">
<div>{% include 'snippets/search_result_text.html' with result=result link=False %}</div>
<button type="submit" class="button is-small is-link">Import book</button>
<button type="submit" class="button is-small is-link">{% trans "Import book" %}</button>
</form>
</li>
{% endfor %}
@ -66,9 +67,9 @@
</div>
<div class="column">
<section class="block">
<h2 class="title">Matching Users</h2>
<h2 class="title">{% trans "Matching Users" %}</h2>
{% if not user_results %}
<p>No users found for "{{ query }}"</p>
<p>{% blocktrans %}No users found for "{{ query }}"{% endblocktrans %}</p>
{% endif %}
<ul>
{% for result in user_results %}
@ -81,9 +82,9 @@
</ul>
</section>
<section class="block">
<h2 class="title">Lists</h2>
<h2 class="title">{% trans "Lists" %}</h2>
{% if not list_results %}
<p>No lists found for "{{ query }}"</p>
<p>{% blocktrans %}No lists found for "{{ query }}"{% endblocktrans %}</p>
{% endif %}
{% for result in list_results %}
<div class="block">

View file

@ -1,4 +1,5 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block content %}
<header class="block column is-offset-one-quarter pl-1">
@ -8,30 +9,30 @@
<div class="block columns">
<nav class="menu column is-one-quarter">
{% if perms.bookwyrm.create_invites %}
<h2 class="menu-label">Manage Users</h2>
<h2 class="menu-label">{% trans "Manage Users" %}</h2>
<ul class="menu-list">
<li>
{% url 'settings-invites' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>Invites</a>
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Invites" %}</a>
</li>
<li>
{% url 'settings-federation' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>Federated Servers</a>
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Federated Servers" %}</a>
</li>
</ul>
{% endif %}
{% if perms.bookwyrm.edit_instance_settings %}
<h2 class="menu-label">Instance Settings</h2>
<h2 class="menu-label">{% trans "Instance Settings" %}</h2>
<ul class="menu-list">
<li>
{% url 'settings-site' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>Site Configuration</a>
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Site Configuration" %}</a>
{% if url in request.path %}
<ul class="emnu-list">
<li><a href="{{ url }}#instance-info">Instance Info</a></li>
<li><a href="{{ url }}#images">Images</a></li>
<li><a href="{{ url }}#footer">Footer Content</a></li>
<li><a href="{{ url }}#registration">Registration</a></li>
<li><a href="{{ url }}#instance-info">{% trans "Instance Info" %}</a></li>
<li><a href="{{ url }}#images">{% trans "Images" %}</a></li>
<li><a href="{{ url }}#footer">{% trans "Footer Content" %}</a></li>
<li><a href="{{ url }}#registration">{% trans "Registration" %}</a></li>
</ul>
{% endif %}
</li>

View file

@ -1,13 +1,14 @@
{% extends 'settings/admin_layout.html' %}
{% block header %}Federated Servers{% endblock %}
{% load i18n %}
{% block header %}{% trans "Federated Servers" %}{% endblock %}
{% block panel %}
<table class="table is-striped">
<tr>
<th>Server name</th>
<th>Software</th>
<th>Status</th>
<th>{% trans "Server name" %}</th>
<th>{% trans "Software" %}</th>
<th>{% trans "Status" %}</th>
</tr>
{% for server in servers %}
<tr>

View file

@ -1,41 +1,42 @@
{% extends 'settings/admin_layout.html' %}
{% block header %}Invites{% endblock %}
{% load i18n %}
{% block header %}{% trans "Invites" %}{% endblock %}
{% load humanize %}
{% block panel %}
<section class="block">
<h2 class="title is-4">Generate New Invite</h2>
<h2 class="title is-4">{% trans "Generate New Invite" %}</h2>
<form name="invite" action="{% url 'settings-invites' %}" method="post">
{% csrf_token %}
<div class="field is-grouped">
<div class="control">
<label class="label" for="id_expiry">Expiry:</label>
<label class="label" for="id_expiry">{% trans "Expiry:" %}</label>
<div class="select">
{{ form.expiry }}
</div>
</div>
<div class="control">
<label class="label" for="id_use_limit">Use limit:</label>
<label class="label" for="id_use_limit">{% trans "Use limit:" %}</label>
<div class="select">
{{ form.use_limit }}
</div>
</div>
</div>
<button class="button is-primary" type="submit">Create Invite</button>
<button class="button is-primary" type="submit">{% trans "Create Invite" %}</button>
</form>
</section>
<section class="block">
<table class="table is-striped">
<tr>
<th>Link</th>
<th>Expires</th>
<th>Max uses</th>
<th>Times used</th>
<th>{% trans "Link" %}</th>
<th>{% trans "Expires" %}</th>
<th>{% trans "Max uses" %}</th>
<th>{% trans "Times used" %}</th>
</tr>
{% if not invites %}
<tr><td colspan="4">No active invites</td></tr>
<tr><td colspan="4">{% trans "No active invites" %}</td></tr>
{% endif %}
{% for invite in invites %}
<tr>

View file

@ -1,45 +1,50 @@
{% extends 'settings/admin_layout.html' %}
{% block header %}Site Configuration{% endblock %}
{% load i18n %}
{% block header %}{% trans "Site Configuration" %}{% endblock %}
{% block panel %}
<form action="{% url 'settings-site' %}" method="POST" class="content">
{% csrf_token %}
<section class="block" id="instance-info">
<h2 class="title is-4">Instance Info</h2>
<h2 class="title is-4">{% trans "Instance Info" %}</h2>
<div class="control">
<label class="label" for="id_name">Instance Name:</label>
<label class="label" for="id_name">{% trans "Instance Name:" %}</label>
{{ site_form.name }}
</div>
<div class="control">
<label class="label" for="id_instance_tagline">Tagline:</label>
<label class="label" for="id_instance_tagline">{% trans "Tagline:" %}</label>
{{ site_form.instance_tagline }}
</div>
<div class="control">
<label class="label" for="id_instance_description">Instance description:</label>
<label class="label" for="id_instance_description">{% trans "Instance description:" %}</label>
{{ site_form.instance_description }}
</div>
<div class="control">
<label class="label" for="id_code_of_conduct">Code of conduct:</label>
<label class="label" for="id_code_of_conduct">{% trans "Code of conduct:" %}</label>
{{ site_form.code_of_conduct }}
</div>
<div class="control">
<label class="label" for="id_privacy_policy">{% trans "Privacy Policy:" %}</label>
{{ site_form.privacy_policy }}
</div>
</section>
<hr aria-hidden="true">
<section class="block" id="images">
<h2 class="title is-4">Images</h2>
<h2 class="title is-4">{% trans "Images" %}</h2>
<div class="field is-grouped">
<div class="control">
<label class="label" for="id_logo">Logo:</label>
<label class="label" for="id_logo">{% trans "Logo:" %}</label>
{{ site_form.logo }}
</div>
<div class="control">
<label class="label" for="id_logo_small">Logo small:</label>
<label class="label" for="id_logo_small">{% trans "Logo small:" %}</label>
{{ site_form.logo_small }}
</div>
<div class="control">
<label class="label" for="id_favicon">Favicon:</label>
<label class="label" for="id_favicon">{% trans "Favicon:" %}</label>
{{ site_form.favicon }}
</div>
</div>
@ -48,17 +53,17 @@
<hr aria-hidden="true">
<section class="block" id="footer">
<h2 class="title is-4">Footer Content</h2>
<h2 class="title is-4">{% trans "Footer Content" %}</h2>
<div class="control">
<label class="label" for="id_support_link">Support link:</label>
<label class="label" for="id_support_link">{% trans "Support link:" %}</label>
<input type="text" name="support_link" maxlength="255" class="input" id="id_support_link" placeholder="https://www.patreon.com/bookwyrm"{% if site.support_link %} value="{{ site.support_link }}"{% endif %}>
</div>
<div class="control">
<label class="label" for="id_support_title">Support title:</label>
<label class="label" for="id_support_title">{% trans "Support title:" %}</label>
<input type="text" name="support_title" maxlength="100" class="input" id="id_support_title" placeholder="Patreon"{% if site.support_title %} value="{{ site.support_title }}"{% endif %}>
</div>
<div class="control">
<label class="label" for="id_admin_email">Admin email:</label>
<label class="label" for="id_admin_email">{% trans "Admin email:" %}</label>
{{ site_form.admin_email }}
</div>
</section>
@ -66,19 +71,19 @@
<hr aria-hidden="true">
<section class="block" id="registration">
<h2 class="title is-4">Registration</h2>
<h2 class="title is-4">{% trans "Registration" %}</h2>
<div class="control">
<label class="label" for="id_allow_registration">Allow registration:
<label class="label" for="id_allow_registration">{% trans "Allow registration:" %}
{{ site_form.allow_registration }}
</div>
<div class="control">
<label class="label" for="id_registration_closed_text">Registration closed text:</label>
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
{{ site_form.registration_closed_text }}
</div>
</section>
<footer class="block">
<button class="button is-primary" type="submit">Save Changes</button>
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
</footer>
</form>
{% endblock %}

View file

@ -1,11 +1,12 @@
{% load i18n %}
{% if not user in request.user.blocks.all %}
<form name="blocks" method="post" action="/block/{{ user.id }}">
{% csrf_token %}
<button class="button is-danger is-light is-small {{ class }}" type="submit">Block</button>
<button class="button is-danger is-light is-small {{ class }}" type="submit">{% trans "Block" %}</button>
</form>
{% else %}
<form name="unblocks" method="post" action="/unblock/{{ user.id }}">
{% csrf_token %}
<button class="button is-small {{ class }}" type="submit">Un-block</button>
<button class="button is-small {{ class }}" type="submit">{% trans "Un-block" %}</button>
</form>
{% endif %}

View file

@ -1,19 +1,20 @@
{% load bookwyrm_tags %}
{% load i18n %}
{% with status.id|uuid as uuid %}
<form name="boost" action="/boost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} {% if request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
{% csrf_token %}
<button class="button is-small" type="submit" {% if not status.boostable %}disabled{% endif %}>
<span class="icon icon-boost" title="Boost status">
<span class="is-sr-only">Boost status</span>
<span class="icon icon-boost" title="{% trans 'Boost status' %}">
<span class="is-sr-only">{% trans "Boost status" %}</span>
</span>
</button>
</form>
<form name="unboost" action="/unboost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} active {% if not request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
{% csrf_token %}
<button class="button is-small is-primary" type="submit">
<span class="icon icon-boost" title="Un-boost status">
<span class="is-sr-only">Un-boost status</span>
<span class="icon icon-boost" title="{% trans 'Un-boost status' %}">
<span class="is-sr-only">{% trans "Un-boost status" %}</span>
</span>
</button>
</form>

View file

@ -1,4 +1,5 @@
{% load i18n %}
<div class="control{% if not parent_status.content_warning %} hidden{% endif %}" id="spoilers-{{ uuid }}">
<label class="is-sr-only" for="id_content_warning-{{ uuid }}">Spoiler alert:</label>
<input type="text" name="content_warning" maxlength="255" class="input" id="id_content_warning-{{ uuid }}" placeholder="Spoilers ahead!"{% if parent_status.content_warning %} value="{{ parent_status.content_warning }}"{% endif %}>
<label class="is-sr-only" for="id_content_warning-{{ uuid }}">{% trans "Spoiler alert:" %}</label>
<input type="text" name="content_warning" maxlength="255" class="input" id="id_content_warning-{{ uuid }}" placeholder="{% trans 'Spoilers ahead!' %}"{% if parent_status.content_warning %} value="{{ parent_status.content_warning }}"{% endif %}>
</div>

View file

@ -1,16 +1,17 @@
{% load humanize %}
{% load i18n %}
{% load bookwyrm_tags %}
<div class="tabs is-boxed">
<ul role="tablist">
<li class="tab-change is-active" data-category="tab-option-{{ book.id }}" role="tab" aria-selected="true" tabindex="0" data-tab="review-{{ book.id }}">
<a>Review</a>
<a>{% trans "Review" %}</a>
</li>
<li class="tab-change" data-category="tab-option-{{ book.id }}" role="tab" tabindex="0" data-tab="comment-{{ book.id}}">
<a>Comment</a>
<a>{% trans "Comment" %}</a>
</li>
<li class="tab-change" data-category="tab-option-{{ book.id }}" role="tab" tabindex="0" data-tab="quote-{{ book.id }}">
<a>Quote</a>
<a>{% trans "Quote" %}</a>
</li>
</ul>
</div>

View file

@ -1,4 +1,5 @@
{% load bookwyrm_tags %}
{% load i18n %}
<form class="is-flex-grow-1" name="{{ type }}" action="/post/{% if type == 'direct' %}status{% else %}{{ type }}{% endif %}" method="post" id="tab-{{ type }}-{{ book.id }}{{ reply_parent.id }}">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
@ -6,7 +7,7 @@
<input type="hidden" name="reply_parent" value="{{ reply_parent.id }}">
{% if type == 'review' %}
<div class="control">
<label class="label" for="id_name_{{ book.id }}_{{ type }}">Title:</label>
<label class="label" for="id_name_{{ book.id }}_{{ type }}">{% trans "Title:" %}</label>
<input type="text" name="name" maxlength="255" class="input" required="" id="id_name_{{ book.id }}_{{ type }}" placeholder="My {{ type }} of '{{ book.title }}'">
</div>
{% endif %}
@ -17,9 +18,9 @@
{% if type == 'review' %}
<fieldset>
<legend class="is-sr-only">Rating</legend>
<legend class="is-sr-only">{% trans "Rating" %}</legend>
<div class="field is-grouped stars form-rate-stars">
<label class="is-sr-only" for="no-rating-{{ book.id }}">No rating</label>
<label class="is-sr-only" for="no-rating-{{ book.id }}">{% trans "No rating" %}</label>
<input class="is-sr-only" type="radio" name="rating" value="" id="no-rating-{{ book.id }}" checked>
{% for i in '12345'|make_list %}
<input class="is-sr-only" id="book{{book.id}}-star-{{ forloop.counter }}" type="radio" name="rating" value="{{ forloop.counter }}">
@ -40,7 +41,7 @@
</div>
{% if type == 'quotation' %}
<div class="control">
<label class="label" for="id_content_quote-{{ book.id }}">Comment:</label>
<label class="label" for="id_content_quote-{{ book.id }}">{% trans "Comment:" %}</label>
{% include 'snippets/content_warning_field.html' with parent_status=status %}
<textarea name="content" class="textarea is-small" id="id_content_quote-{{ book.id }}"></textarea>
</div>
@ -55,14 +56,14 @@
<div class="control">
{% if type == 'direct' %}
<input type="hidden" name="privacy" value="direct">
<button type="button" class="button" aria-label="Privacy" disabled>Private</button>
<button type="button" class="button" aria-label="Privacy" disabled>{% trans "Private" %}</button>
{% else %}
{% include 'snippets/privacy_select.html' with current=reply_parent.privacy %}
{% endif %}
</div>
</div>
<div class="column is-narrow">
<button class="button is-link" type="submit">Post</button>
<button class="button is-link" type="submit">{% trans "Post" %}</button>
</div>
</div>
</form>

View file

@ -1,8 +1,10 @@
{% extends 'components/modal.html' %}
{% block modal-title %}Delete these read dates?{% endblock %}
{% load i18n %}
{% block modal-title %}{% trans "Delete these read dates?" %}{% endblock %}
{% block modal-body %}
{% if readthrough.progress_updates|length > 0 %}
You are deleting this readthrough and its {{ readthrough.progress_updates|length }} associated progress updates.
{% blocktrans with count=readthrough.progress_updates|length %}You are deleting this readthrough and its {{ count }} associated progress updates.{% endblocktrans %}
{% endif %}
{% endblock %}
{% block modal-footer %}
@ -10,7 +12,7 @@ You are deleting this readthrough and its {{ readthrough.progress_updates|length
{% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}">
<button class="button is-danger" type="submit">
Delete
{% trans "Delete" %}
</button>
{% include 'snippets/toggle/toggle_button.html' with text="Cancel" controls_text="delete-readthrough" controls_uid=readthrough.id %}
</form>

View file

@ -1,18 +1,19 @@
{% load bookwyrm_tags %}
{% load i18n %}
{% with status.id|uuid as uuid %}
<form name="favorite" action="/favorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} {% if request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
{% csrf_token %}
<button class="button is-small" type="submit">
<span class="icon icon-heart" title="Like status">
<span class="is-sr-only">Like status</span>
<span class="icon icon-heart" title="{% trans 'Like status' %}">
<span class="is-sr-only">{% trans "Like status" %}</span>
</span>
</button>
</form>
<form name="unfavorite" action="/unfavorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} active {% if not request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
{% csrf_token %}
<button class="button is-primary is-small" type="submit">
<span class="icon icon-heart" title="Un-like status">
<span class="is-sr-only">Un-like status</span>
<span class="icon icon-heart" title="{% trans 'Un-like status' %}">
<span class="is-sr-only">{% trans "Un-like status" %}</span>
</span>
</button>
</form>

View file

@ -1,8 +1,9 @@
{% load i18n %}
{% if request.user == user or not request.user.is_authenticated %}
{% elif request.user in user.follower_requests.all %}
<div>
Follow request already sent.
{% trans "Follow request already sent." %}
</div>
{% elif user in request.user.blocks.all %}
@ -15,15 +16,15 @@ Follow request already sent.
{% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}">
{% if user.manually_approves_followers %}
<button class="button is-small is-link" type="submit">Send follow request</button>
<button class="button is-small is-link" type="submit">{% trans "Send follow request" %}</button>
{% else %}
<button class="button is-small is-link" type="submit">Follow</button>
<button class="button is-small is-link" type="submit">{% trans "Follow" %}</button>
{% endif %}
</form>
<form action="/unfollow/" method="POST" class="interaction follow-{{ user.id }} {% if not request.user in user.followers.all %}hidden{%endif %}" data-id="follow-{{ user.id }}">
{% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}">
<button class="button is-small is-danger is-light" type="submit">Unfollow</button>
<button class="button is-small is-danger is-light" type="submit">{% trans "Unfollow" %}</button>
</form>
</div>
<div class="control">

View file

@ -1,15 +1,16 @@
{% load i18n %}
{% load bookwyrm_tags %}
{% if request.user|follow_request_exists:user %}
<div class="field is-grouped">
<form action="/accept-follow-request/" method="POST">
{% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}">
<button class="button is-link is-small" type="submit">Accept</button>
<button class="button is-link is-small" type="submit">{% trans "Accept" %}</button>
</form>
<form action="/delete-follow-request/" method="POST">
{% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}">
<button class="button is-danger is-light is-small" type="submit" class="warning">Delete</button>
<button class="button is-danger is-light is-small" type="submit" class="warning">{% trans "Delete" %}</button>
</form>
</div>
{% endif %}

View file

@ -1,15 +1,16 @@
{% extends 'components/card.html' %}
{% load i18n %}
{% block card-header %}
<h3 class="card-header-title has-background-primary has-text-white">
<span class="icon icon-book is-size-3 mr-2" aria-hidden="true"></span> {{ year }} reading goal
<span class="icon icon-book is-size-3 mr-2" aria-hidden="true"></span> {% blocktrans %}{{ year }} reading goal{% endblocktrans %}
</h3>
{% endblock %}
{% block card-content %}
<div class="content">
<p>Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.</p>
<p>{% blocktrans %}Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.{% endblocktrans %}</p>
{% include 'snippets/goal_form.html' %}
</div>
@ -17,7 +18,7 @@
{% block card-footer %}
<div class="card-footer-item is-flex-direction-column">
<button class="button is-danger is-light is-block set-display" data-id="hide-{{ year }}-reading-goal" data-value="true">Dismiss message</button>
<p class="help">You can set or change your reading goal any time from your <a href="{{ request.user.local_path }}">profile page</a></p>
<button class="button is-danger is-light is-block set-display" data-id="hide-{{ year }}-reading-goal" data-value="true">{% trans "Dismiss message" %}</button>
<p class="help">{% blocktrans with path=request.user.local_path %}You can set or change your reading goal any time from your <a href="{{ path }}">profile page</a>{% endblocktrans %}</p>
</div>
{% endblock %}

View file

@ -1,3 +1,4 @@
{% load i18n %}
<form method="post" name="goal" action="{{ request.user.local_path }}/goal/{{ year }}">
{% csrf_token %}
<input type="hidden" name="year" value="{% if goal %}{{ goal.year }}{% else %}{{ year }}{% endif %}">
@ -5,28 +6,28 @@
<div class="columns">
<div class="column">
<label class="label" for="id_goal">Reading goal:</label>
<label class="label" for="id_goal">{% trans "Reading goal:" %}</label>
<div class="field has-addons">
<div class="control">
<input type="number" class="input" name="goal" min="1" id="id_goal" value="{% if goal %}{{ goal.goal }}{% else %}12{% endif %}">
</div>
<p class="button is-static" aria-hidden="true">books</p>
<p class="button is-static" aria-hidden="true">{% trans "books" %}</p>
</div>
</div>
<div class="column">
<label class="label"><p class="mb-2">Goal privacy:</p>
<label class="label"><p class="mb-2">{% trans "Goal privacy:" %}</p>
{% include 'snippets/privacy_select.html' with no_label=True current=goal.privacy %}
</label>
</div>
</div>
<label for="post_status" class="label">
<input type="checkbox" name="post-status" id="post_status" class="checkbox" checked>
Post to feed
{% trans "Post to feed" %}
</label>
<p>
<button type="submit" class="button is-link">Set goal</button>
<button type="submit" class="button is-link">{% trans "Set goal" %}</button>
{% if goal %}
{% include 'snippets/toggle/close_button.html' with text="Cancel" controls_text="show-edit-goal" %}
{% endif %}

View file

@ -1,9 +1,10 @@
{% load i18n %}
{% load humanize %}
<p>
{% if goal.progress_percent >= 100 %}
Success!
{% trans "Success!" %}
{% elif goal.progress_percent %}
{{ goal.progress_percent }}% complete!
{% blocktrans with percent=goal.percent %}{{ percent }}% complete!{% endblocktrans %}
{% endif %}
{% if goal.user == request.user %}You've{% else %}{{ goal.user.display_name }} has{% endif %} read {% if request.path != goal.local_path %}<a href="{{ goal.local_path }}">{% endif %}{{ goal.book_count }} of {{ goal.goal | intcomma }} books{% if request.path != goal.local_path %}</a>{% endif %}.
</p>

View file

@ -1,9 +1,10 @@
{% load i18n %}
<nav class="pagination" role="navigation" aria-label="pagination">
{% if page.has_previous %}
<p class="pagination-previous">
<a href="{{ path }}?page={{ page.previous_page_number }}{{ anchor }}">
<span class="icon icon-arrow-left"></span>
Previous
{% trans "Previous" %}
</a>
</p>
{% endif %}
@ -11,7 +12,7 @@
{% if page.has_next %}
<p class="pagination-next">
<a href="{{ path }}?page={{ page.next_page_number }}{{ anchor }}">
Next
{% trans "Next" %}
<span class="icon icon-arrow-right"></span>
</a>
</p>

View file

@ -1,18 +1,19 @@
{% load i18n %}
{% if item.privacy == 'public' %}
<span class="icon icon-globe" title="Public">
<span class="is-sr-only">Public</span>
<span class="icon icon-globe" title="{% trans 'Public' %}">
<span class="is-sr-only">{% trans "Public" %}</span>
</span>
{% elif item.privacy == 'unlisted' %}
<span class="icon icon-unlock" title="Unlisted">
<span class="is-sr-only">Unlisted</span>
<span class="icon icon-unlock" title="{% trans 'Unlisted' %}">
<span class="is-sr-only">{% trans "Unlisted" %}</span>
</span>
{% elif item.privacy == 'followers' %}
<span class="icon icon-lock" title="Followers-only">
<span class="is-sr-only">Followers-only</span>
<span class="is-sr-only">{% trans "Followers-only" %}</span>
</span>
{% else %}
<span class="icon icon-envelope" title="Private">
<span class="is-sr-only">Private</span>
<span class="icon icon-envelope" title="{% trans 'Private' %}">
<span class="is-sr-only">{% trans "Private" %}</span>
</span>
{% endif %}

View file

@ -1,21 +1,22 @@
{% load i18n %}
{% load bookwyrm_tags %}
<div class="select {{ class }}">
{% with 0|uuid as uuid %}
{% if not no_label %}
<label class="is-sr-only" for="privacy-{{ uuid }}">Post privacy</label>
<label class="is-sr-only" for="privacy-{{ uuid }}">{% trans "Post privacy" %}</label>
{% endif %}
<select name="privacy" id="privacy-{{ uuid }}">
<option value="public" {% if not current or current == 'public' %}selected{% endif %}>
Public
{% trans "Public" %}
</option>
<option value="unlisted" {% if current == 'unlisted' %}selected{% endif %}>
Unlisted
{% trans "Unlisted" %}
</option>
<option value="followers" {% if current == 'followers' %}selected{% endif %}>
Followers
{% trans "Followers" %}
</option>
<option value="direct" {% if current == 'direct' %}selected{% endif %}>
Private
{% trans "Private" %}
</option>
</select>
{% endwith %}

View file

@ -1,8 +1,9 @@
{% load i18n %}
<form class="field is-grouped is-small" action="/edit-readthrough" method="POST">
{% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}"/>
<div class="field">
<label class="label is-align-self-center mb-0 pr-2" for="progress">Progress:</label>
<label class="label is-align-self-center mb-0 pr-2" for="progress">{% trans "Progress:" %}</label>
<div class="field has-addons mb-0">
<div class="control">
<input
@ -12,16 +13,16 @@
</div>
<div class="control select is-small">
<select name="progress_mode" aria-label="Progress mode">
<option value="PG" {% if readthrough.progress_mode == 'PG' %}selected{% endif %}>pages</option>
<option value="PCT" {% if readthrough.progress_mode == 'PCT' %}selected{% endif %}>percent</option>
<option value="PG" {% if readthrough.progress_mode == 'PG' %}selected{% endif %}>{% trans "pages" %}</option>
<option value="PCT" {% if readthrough.progress_mode == 'PCT' %}selected{% endif %}>{% trans "percent" %}</option>
</select>
</div>
<div class="control">
<button class="button is-small px-2 is-primary" type="submit">Save</button>
<button class="button is-small px-2 is-primary" type="submit">{% trans "Save" %}</button>
</div>
</div>
{% if readthrough.progress_mode == 'PG' and book.pages %}
<p class="help">of {{ book.pages }} pages</p>
<p class="help">{% blocktrans %}of {{ book.pages }} pages{% endblocktrans %}</p>
{% endif %}
</div>
</form>

View file

@ -1,6 +1,7 @@
{% load i18n %}
{% load bookwyrm_tags %}
{% if request.user.is_authenticated %}
<span class="is-sr-only">Leave a rating</span>
<span class="is-sr-only">{% trans "Leave a rating" %}</span>
<div class="block">
<form class="hidden-form" name="rate" action="/post/rating" method="POST">
{% csrf_token %}
@ -10,7 +11,7 @@
<input type="hidden" name="rating" value="{{ forloop.counter }}">
<div class="field is-grouped stars form-rate-stars mb-1 has-text-warning-dark">
<label class="is-sr-only" for="rating-no-rating-{{ book.id }}">No rating</label>
<label class="is-sr-only" for="rating-no-rating-{{ book.id }}">{% trans "No rating" %}</label>
<input class="is-sr-only" type="radio" name="rating" value="" id="rating-no-rating-{{ book.id }}" checked>
{% for i in '12345'|make_list %}
<input class="is-sr-only" id="rating-book{{book.id}}-star-{{ forloop.counter }}" type="radio" name="rating" value="{{ forloop.counter }}" {% if book|user_rating:user == forloop.counter %}checked{% endif %}>
@ -25,7 +26,7 @@
{% include 'snippets/privacy_select.html' with class="is-small" %}
</div>
<div class="control">
<button class="button is-small is-primary" type="submit">Rate</button>
<button class="button is-small is-primary" type="submit">{% trans "Rate" %}</button>
</div>
</div>
</form>

View file

@ -1,13 +1,14 @@
{% load i18n %}
{% load humanize %}
<div class="content block">
<div id="hide-edit-readthrough-{{ readthrough.id }}">
<div class="columns">
<div class="column">
Progress Updates:
{% trans "Progress Updates:" %}
</dl>
<ul>
{% if readthrough.finish_date or readthrough.progress %}
<li>{% if readthrough.finish_date %} {{ readthrough.finish_date | naturalday }}: finished {% else %}{% if readthrough.progress_mode == 'PG' %}on page {{ readthrough.progress }}{% if book.pages %} of {{ book.pages }}{% endif %}
<li>{% if readthrough.finish_date %} {{ readthrough.finish_date | naturalday }}: {% trans "finished" %} {% else %}{% if readthrough.progress_mode == 'PG' %}on page {{ readthrough.progress }}{% if book.pages %} of {{ book.pages }}{% endif %}
{% else %}{{ readthrough.progress }}%{% endif %}{% endif %}
{% if readthrough.progress %}
{% include 'snippets/toggle/toggle_button.html' with text="Show all updates" controls_text="updates" controls_uid=readthrough.id class="is-small" %}
@ -25,7 +26,7 @@
<input type="hidden" name="id" value="{{ progress_update.id }}"/>
<button type="submit" class="button is-small" for="delete-progressupdate-{{ progress_update.id }}" role="button" tabindex="0">
<span class="icon icon-x" title="Delete this progress update">
<span class="is-sr-only">Delete this progress update</span>
<span class="is-sr-only">{% trans "Delete this progress update" %}</span>
</span>
</button>
</form>
@ -35,7 +36,7 @@
{% endif %}
</li>
{% endif %}
<li>{{ readthrough.start_date | naturalday }}: started</li>
<li>{{ readthrough.start_date | naturalday }}: {% trans "started" %}</li>
</ul>
</div>
<div class="column is-narrow">
@ -53,11 +54,11 @@
</div>
<div class="box hidden" id="edit-readthrough-{{ readthrough.id }}" tabindex="0">
<h3 class="title is-5">Edit read dates</h3>
<h3 class="title is-5">{% trans "Edit read dates" %}</h3>
<form name="edit-readthrough" action="/edit-readthrough" method="post">
{% include 'snippets/readthrough_form.html' with readthrough=readthrough %}
<div class="field is-grouped">
<button class="button is-primary" type="submit">Save</button>
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
{% include 'snippets/toggle/close_button.html' with text="Cancel" controls_text="edit-readthrough" controls_uid=readthrough.id %}
</div>
</form>

View file

@ -1,16 +1,17 @@
{% load i18n %}
{% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}">
<input type="hidden" name="book" value="{{ book.id }}">
<div class="field">
<label class="label">
Started reading
{% trans "Started reading" %}
<input type="date" name="start_date" class="input" id="id_start_date-{{ readthrough.id }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
</label>
</div>
{# Only show progress for editing existing readthroughs #}
{% if readthrough.id and not readthrough.finish_date %}
<label class="label" for="id_progress-{{ readthrough.id }}">
Progress
{% trans "Progress" %}
</label>
<div class="field has-addons">
<div class="control">
@ -18,15 +19,15 @@
</div>
<div class="control select">
<select name="progress_mode" aria-label="Progress mode">
<option value="PG" {% if readthrough.progress_mode == 'PG' %}selected{% endif %}>pages</option>
<option value="PCT" {% if readthrough.progress_mode == 'PCT' %}selected{% endif %}>percent</option>
<option value="PG" {% if readthrough.progress_mode == 'PG' %}selected{% endif %}>{% trans "pages" %}</option>
<option value="PCT" {% if readthrough.progress_mode == 'PCT' %}selected{% endif %}>{% trans "percent" %}</option>
</select>
</div>
</div>
{% endif %}
<div class="field">
<label class="label">
Finished reading
{% trans "Finished reading" %}
<input type="date" name="finish_date" class="input" id="id_finish_date-{{ readthrough.id }}" value="{{ readthrough.finish_date | date:"Y-m-d" }}">
</label>
</div>

View file

@ -1,6 +1,7 @@
{% load i18n %}
{% csrf_token %}
<div class="field">
<label class="label" for="id_localname_register">Username:</label>
<label class="label" for="id_localname_register">{% trans "Username:" %}</label>
<div class="control">
<input type="text" name="localname" maxlength="150" class="input" required="" id="id_localname_register" value="{% if register_form.localname.value %}{{ register_form.localname.value }}{% endif %}">
</div>
@ -9,7 +10,7 @@
{% endfor %}
</div>
<div class="field">
<label class="label" for="id_email_register">Email address:</label>
<label class="label" for="id_email_register">{% trans "Email address:" %}</label>
<div class="control">
<input type="email" name="email" maxlength="254" class="input" id="id_email_register" value="{% if register_form.email.value %}{{ register_form.email.value }}{% endif %}">
{% for error in register_form.email.errors %}
@ -18,7 +19,7 @@
</div>
</div>
<div class="field">
<label class="label" for="id_password_register">Password:</label>
<label class="label" for="id_password_register">{% trans "Password:" %}</label>
<div class="control">
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_register">
{% for error in register_form.password.errors %}
@ -28,6 +29,6 @@
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-primary" type="submit">Sign Up</button>
<button class="button is-primary" type="submit">{% trans "Sign Up" %}</button>
</div>
</div>

View file

@ -1,15 +1,16 @@
{% load i18n %}
{{ obj.user.display_name }}{% if obj.status_type == 'GeneratedNote' %}
{{ obj.content | safe }}
{% elif obj.status_type == 'Review' and not obj.name and not obj.content%}
rated
{% trans "rated" %}
{% elif obj.status_type == 'Review' %}
reviewed
{% trans "reviewed" %}
{% elif obj.status_type == 'Comment' %}
commented on
{% trans "commented on" %}
{% elif obj.status_type == 'Quotation' %}
quoted
{% trans "quoted" %}
{% endif %}
{% if obj.book %}{{ obj.book.title | safe}}
{% elif obj.mention_books %}
{{ obj.mention_books.first.title }}
{% endif %}
{% endif %}

View file

@ -1,2 +1,3 @@
{% load i18n %}
<strong>{% if link %}<a href="{{ result.key }}">{{ result.title }}</a>{% else %}{{ result.title }}{% endif %}</strong>
{% if result.author %} by {{ result.author }}{% endif %}{% if result.year %} ({{ result.year }}){% endif %}
{% if result.author %} {% blocktrans with author=result.author %}by {{ author }}{% endblocktrans %}{% endif %}{% if result.year %} ({{ result.year }}){% endif %}

View file

@ -1,39 +1,20 @@
{% load humanize %}
{% load i18n %}
{% load bookwyrm_tags %}
{% if books|length > 0 %}
<div class="table-container">
<table class="table is-striped is-fullwidth">
<tr class="book-preview">
<th>
Cover
</th>
<th>
Title
</th>
<th>
Author
</th>
<th>
Published
</th>
<th>
Shelved
</th>
<th>
Started
</th>
<th>
Finished
</th>
<th>
External links
</th>
{% if ratings %}
<th>
Rating
</th>
{% endif %}
<th>{% trans "Cover" %}</th>
<th>{% trans "Title" %}</th>
<th>{% trans "Author" %}</th>
<th>{% trans "Published" %}</th>
<th>{% trans "Shelved" %}</th>
<th>{% trans "Started" %}</th>
<th>{% trans "Finished" %}</th>
<th>{% trans "External links" %}</th>{% if ratings %}
<th>{% trans "Rating" %}</th>{% endif %}
</tr>
{% for book in books %}
<tr class="book-preview">
@ -60,7 +41,7 @@
{{ read_through.finish_date | naturalday |default_if_none:""}}
</td>
<td>
<a href="https://openlibrary.org/book/{{ book.openlibrary_key }}" target="_blank">OpenLibrary</a>
<a href="https://openlibrary.org/book/{{ book.openlibrary_key }}" target="_blank">{% trans "OpenLibrary" %}</a>
</td>
{% if ratings %}
<td>
@ -77,13 +58,13 @@
</table>
</div>
{% else %}
<p>This shelf is empty.</p>
<p>{% trans "This shelf is empty." %}</p>
{% if shelf.editable %}
<form name="delete-shelf" action="/delete-shelf/{{ shelf.id }}" method="post">
{% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}">
<button class="button is-danger is-light" type="submit">
Delete shelf
{% trans "Delete shelf" %}
</button>
</form>
{% endif %}

View file

@ -1,6 +1,7 @@
{% extends 'components/dropdown.html' %}
{% load i18n %}
{% block dropdown-trigger %}
<span>Change shelf</span>
<span>{% trans "Change shelf" %}</span>
<span class="icon icon-arrow-down" aria-hidden="true"></span>
{% endblock %}
@ -23,7 +24,7 @@
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="shelf" value="{{ current.id }}">
<button class="button is-fullwidth is-small is-danger is-light" type="submit">Unshelve</button>
<button class="button is-fullwidth is-small is-danger is-light" type="submit">{% trans "Unshelve" %}</button>
</form>
</li>
{% endblock %}

View file

@ -1,7 +1,8 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% block modal-title %}
Finish "<em>{{ book.title }}</em>"
{% blocktrans with book_title=book.title %}Finish "<em>{{ book_title }}</em>"{% endblocktrans %}
{% endblock %}
@ -15,13 +16,13 @@ Finish "<em>{{ book.title }}</em>"
<input type="hidden" name="id" value="{{ readthrough.id }}">
<div class="field">
<label class="label">
Started reading
{% trans "Started reading" %}
<input type="date" name="start_date" class="input" id="finish_id_start_date-{{ uuid }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
</label>
</div>
<div class="field">
<label class="label">
Finished reading
{% trans "Finished reading" %}
<input type="date" name="finish_date" class="input" id="id_finish_date-{{ uuid }}" value="{% now "Y-m-d" %}">
</label>
</div>
@ -33,12 +34,12 @@ Finish "<em>{{ book.title }}</em>"
<div class="column field">
<label for="post_status-{{ uuid }}">
<input type="checkbox" name="post-status" class="checkbox" id="post_status-{{ uuid }}" checked>
Post to feed
{% trans "Post to feed" %}
</label>
{% include 'snippets/privacy_select.html' %}
</div>
<div class="column">
<button type="submit" class="button is-success">Save</button>
<button type="submit" class="button is-success">{% trans "Save" %}</button>
{% include 'snippets/toggle/close_button.html' with text="Cancel" controls_text="finish-reading" controls_uid=uuid %}
</div>
</div>

View file

@ -1,7 +1,8 @@
{% extends 'components/dropdown.html' %}
{% load i18n %}
{% block dropdown-trigger %}
<span class="icon icon-arrow-down">
<span class="is-sr-only">More shelves</span>
<span class="is-sr-only">{% trans "More shelves" %}</span>
</span>
{% endblock %}

View file

@ -1,4 +1,5 @@
{% load bookwyrm_tags %}
{% load i18n %}
{% for shelf in shelves %}
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
{% if dropdown %}<li role="menuitem">{% endif %}
@ -6,7 +7,7 @@
{% if shelf.identifier == 'reading' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}
{% include 'snippets/toggle/toggle_button.html' with class=class text="Start reading" controls_text="start-reading" controls_uid=button_uuid focus="modal-title-start-reading" disabled=is_current %}
{% endif %}{% elif shelf.identifier == 'read' and active_shelf.shelf.identifier == 'read' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}
<button type="button" class="button {{ class }}" disabled><span>Read</span>
<button type="button" class="button {{ class }}" disabled><span>{% trans "Read" %}</span>
{% endif %}{% elif shelf.identifier == 'read' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}
{% include 'snippets/toggle/toggle_button.html' with class=class text="Finish reading" controls_text="finish-reading" controls_uid=button_uuid focus="modal-title-finish-reading" disabled=is_current %}
{% endif %}{% elif shelf.identifier == 'to-read' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}

View file

@ -1,7 +1,8 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% block modal-title %}
Start "<em>{{ book.title }}</em>"
{% blocktrans with book_title=book.title %}Start "<em>{{ book_title }}</em>"{% endblocktrans %}
{% endblock %}
{% block modal-form-open %}
@ -13,7 +14,7 @@ Start "<em>{{ book.title }}</em>"
{% csrf_token %}
<div class="field">
<label class="label">
Started reading
{% trans "Started reading" %}
<input type="date" name="start_date" class="input" id="start_id_start_date-{{ uuid }}" value="{% now "Y-m-d" %}">
</label>
</div>
@ -25,12 +26,12 @@ Start "<em>{{ book.title }}</em>"
<div class="column field">
<label for="post_status_start-{{ uuid }}">
<input type="checkbox" name="post-status" class="checkbox" id="post_status_start-{{ uuid }}" checked>
Post to feed
{% trans "Post to feed" %}
</label>
{% include 'snippets/privacy_select.html' %}
</div>
<div class="column">
<button class="button is-success" type="submit">Save</button>
<button class="button is-success" type="submit">{% trans "Save" %}</button>
{% include 'snippets/toggle/toggle_button.html' with text="Cancel" controls_text="start-reading" controls_uid=uuid %}
</div>
</div>

View file

@ -1,7 +1,8 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% block modal-title %}
Want to Read "<em>{{ book.title }}</em>"
{% blocktrans with book_title=book.title %}Want to Read "<em>{{ book_title }}</em>"{% endblocktrans %}
{% endblock %}
{% block modal-form-open %}
@ -16,13 +17,13 @@ Want to Read "<em>{{ book.title }}</em>"
<div class="column field">
<label for="post_status_want-{{ uuid }}">
<input type="checkbox" name="post-status" class="checkbox" id="post_status_want-{{ uuid }}" checked>
Post to feed
{% trans "Post to feed" %}
</label>
{% include 'snippets/privacy_select.html' %}
</div>
<div class="column">
<button class="button is-success" type="submit">
<span>Want to read</span>
<span>{% trans "Want to read" %}</span>
</button>
{% include 'snippets/toggle/toggle_button.html' with text="Cancel" controls_text="want-to-read" controls_uid=uuid %}
</div>

View file

@ -1,5 +1,6 @@
{% load i18n %}
<p class="stars">
<span class="is-sr-only">{% if rating %}{{ rating|floatformat }} star{{ rating|floatformat | pluralize }}{% else %}No rating{% endif %}</span>
<span class="is-sr-only">{% if rating %}{{ rating|floatformat }} star{{ rating|floatformat | pluralize }}{% else %}{% trans "No rating" %}{% endif %}</span>
{% for i in '12345'|make_list %}
<span class="icon is-small mr-1 icon-star-{% if rating >= forloop.counter %}full{% elif rating|floatformat:0 >= forloop.counter|floatformat:0 %}half{% else %}empty{% endif %}" aria-hidden="true">
</span>

View file

@ -1,9 +1,10 @@
{% load bookwyrm_tags %}
{% load i18n %}
{% if not status.deleted %}
{% if status.status_type == 'Boost' %}
{% include 'snippets/avatar.html' with user=status.user %}
{% include 'snippets/username.html' with user=status.user %}
boosted
{% trans "boosted" %}
{% include 'snippets/status/status_body.html' with status=status|boosted_status %}
{% else %}
{% include 'snippets/status/status_body.html' with status=status %}

View file

@ -1,4 +1,5 @@
{% extends 'components/card.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
@ -32,16 +33,16 @@
{% else %}
<a href="/login">
<span class="icon icon-comment" title="Reply">
<span class="is-sr-only">Reply</span>
<span class="icon icon-comment" title="{% trans 'Reply' %}">
<span class="is-sr-only">{% trans "Reply" %}</span>
</span>
<span class="icon icon-boost" title="Boost status">
<span class="is-sr-only">Boost status</span>
<span class="icon icon-boost" title="{% trans 'Boost status' %}">
<span class="is-sr-only">{% trans "Boost status" %}</span>
</span>
<span class="icon icon-heart" title="Like status">
<span class="is-sr-only">Like status</span>
<span class="icon icon-heart" title="{% trans 'Like status' %}">
<span class="is-sr-only">{% trans "Like status" %}</span>
</span>
</a>
{% endif %}

View file

@ -1,4 +1,5 @@
{% load bookwyrm_tags %}
{% load i18n %}
<div class="block">
{% if status.status_type == 'Review' %}
<div>
@ -38,7 +39,7 @@
{% for attachment in status.attachments.all %}
<div class="column is-narrow">
<figure class="image is-128x128">
<a href="/images/{{ attachment.image }}" target="_blank" aria-label="open image in new window">
<a href="/images/{{ attachment.image }}" target="_blank" aria-label="{% trans 'Open image in new window' %}">
<img src="/images/{{ attachment.image }}"{% if attachment.caption %} alt="{{ attachment.caption }}" title="{{ attachment.caption }}"{% endif %}>
</a>
</figure>

View file

@ -1,17 +1,18 @@
{% load bookwyrm_tags %}
{% load i18n %}
{% include 'snippets/avatar.html' with user=status.user %}
{% include 'snippets/username.html' with user=status.user %}
{% if status.status_type == 'GeneratedNote' %}
{{ status.content | safe }}
{% elif status.status_type == 'Review' and not status.name and not status.content%}
rated
{% trans "rated" %}
{% elif status.status_type == 'Review' %}
reviewed
{% trans "reviewed" %}
{% elif status.status_type == 'Comment' %}
commented on
{% trans "commented on" %}
{% elif status.status_type == 'Quotation' %}
quoted
{% trans "quoted" %}
{% elif status.reply_parent %}
{% with parent_status=status|parent %}
replied to {% include 'snippets/username.html' with user=parent_status.user possessive=True %} <a href="{{parent_status.remote_id }}">{% if parent_status.status_type == 'GeneratedNote' %}update{% else %}{{ parent_status.status_type | lower }}{% endif %}</a>

View file

@ -1,9 +1,10 @@
{% extends 'components/dropdown.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block dropdown-trigger %}
<span class="icon icon-dots-three">
<span class="is-sr-only">More options</span>
<span class="is-sr-only">{% trans "More options" %}</span>
</span>
{% endblock %}
@ -13,13 +14,13 @@
<form class="dropdown-item pt-0 pb-0" name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
{% csrf_token %}
<button class="button is-danger is-light is-fullwidth is-small" type="submit">
Delete post
{% trans "Delete post" %}
</button>
</form>
</li>
{% else %}
<li role="menuitem">
<a href="/direct-messages/{{ status.user|username }}" class="button is-fullwidth is-small">Send direct message</a>
<a href="/direct-messages/{{ status.user|username }}" class="button is-fullwidth is-small">{% trans "Send direct message" %}</a>
</li>
<li role="menuitem">
{% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" %}

View file

@ -1,5 +1,6 @@
{% load i18n %}
<form name="switch-edition" action="/switch-edition" method="POST">
{% csrf_token %}
<input type="hidden" name="edition" value="{{ edition.id }}">
<button type="submit" class="button {{ size }}">Switch to this edition</button>
<button type="submit" class="button {{ size }}">{% trans "Switch to this edition" %}</button>
</form>

View file

@ -1,3 +1,4 @@
{% load i18n %}
<div class="control">
<form name="tag" action="/{% if tag.tag.identifier in user_tags %}untag{% else %}tag{% endif %}/" method="post">
{% csrf_token %}
@ -10,11 +11,11 @@
</a>
{% if tag.tag.identifier in user_tags %}
<button class="tag is-delete" type="submit">
<span class="is-sr-only">remove tag</span>
<span class="is-sr-only">{% trans "Remove tag" %}</span>
</button>
{% else %}
<button class="tag" type="submit">+
<span class="is-sr-only">add tag</span>
<span class="is-sr-only">{% trans "Add tag" %}</span>
</button>
{% endif %}
</div>

View file

@ -1,15 +1,16 @@
{% extends 'components/dropdown.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block dropdown-trigger %}
<span class="icon icon-dots-three">
<span class="is-sr-only">More options</span>
<span class="is-sr-only">{% trans "More options" %}</span>
</span>
{% endblock %}
{% block dropdown-list %}
<li role="menuitem">
<a href="/direct-messages/{{ user|username }}" class="button is-fullwidth is-small">Send direct message</a>
<a href="/direct-messages/{{ user|username }}" class="button is-fullwidth is-small">{% trans "Send direct message" %}</a>
</li>
<li role="menuitem">
{% include 'snippets/block_button.html' with user=user class="is-fullwidth" %}

View file

@ -1,9 +1,10 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block content %}
<div class="block">
<h1 class="title">Books tagged "{{ tag.name }}"</h1>
<h1 class="title">{% blocktrans %}Books tagged "{{ tag.name }}"{% endblocktrans %}</h1>
{% include 'snippets/book_tiles.html' with books=books.all %}
</div>

View file

@ -1,7 +1,8 @@
{% extends 'components/inline_form.html' %}
{% load i18n %}
{% block header %}
Create New Shelf
{% trans "Create New Shelf" %}
{% endblock %}
{% block form %}
@ -9,7 +10,7 @@ Create New Shelf
{% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}">
<div class="field">
<label class="label" for="id_name_create">Name:</label>
<label class="label" for="id_name_create">{% trans "Name:" %}</label>
<input type="text" name="name" maxlength="100" class="input" required="true" id="id_name_create">
</div>
@ -18,7 +19,7 @@ Create New Shelf
{% include 'snippets/privacy_select.html' %}
</div>
<div class="control">
<button class="button is-primary" type="submit">Create shelf</button>
<button class="button is-primary" type="submit">{% trans "Create shelf" %}</button>
</div>
</div>
</form>

View file

@ -1,7 +1,8 @@
{% extends 'components/inline_form.html' %}
{% load i18n %}
{% block header %}
Edit Shelf
{% trans "Edit Shelf" %}
{% endblock %}
{% block form %}
@ -10,7 +11,7 @@ Edit Shelf
<input type="hidden" name="user" value="{{ request.user.id }}">
{% if shelf.editable %}
<div class="field">
<label class="label" for="id_name">Name:</label>
<label class="label" for="id_name">{% trans "Name:" %}</label>
<input type="text" name="name" maxlength="100" class="input" required="true" value="{{ shelf.name }}" id="id_name">
</div>
{% else %}
@ -22,7 +23,7 @@ Edit Shelf
{% include 'snippets/privacy_select.html' with current=shelf.privacy %}
</div>
<div class="control">
<button class="button is-primary" type="submit">Update shelf</button>
<button class="button is-primary" type="submit">{% trans "Update shelf" %}</button>
</div>
</div>
</form>

View file

@ -1,4 +1,5 @@
{% extends 'user/user_layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block header %}
@ -13,7 +14,7 @@
{% block panel %}
<div class="block">
<h2 class="title">Followers</h2>
<h2 class="title">{% trans "Followers" %}</h2>
{% for followers in followers %}
<div class="block columns">
<div class="column">
@ -26,7 +27,7 @@
</div>
{% endfor %}
{% if not followers.count %}
<div>{{ user|username }} has no followers</div>
<div>{% blocktrans with username=user|username %}{{ username }} has no followers{% endblocktrans %}</div>
{% endif %}
</div>
{% endblock %}

View file

@ -1,4 +1,5 @@
{% extends 'user/user_layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block header %}
@ -13,7 +14,7 @@
{% block panel %}
<div class="block">
<h2 class="title">Following</h2>
<h2 class="title">{% trans "Following" %}</h2>
{% for follower in user.following.all %}
<div class="block columns">
<div class="column">
@ -26,7 +27,7 @@
</div>
{% endfor %}
{% if not following.count %}
<div>{{ user|username }} isn't following any users</div>
<div>{% blocktrans with username=user|username %}{{ username }} isn't following any users{% endblocktrans %}</div>
{% endif %}
</div>
{% endblock %}

View file

@ -1,4 +1,5 @@
{% extends 'user/user_layout.html' %}
{% load i18n %}
{% block header %}
<div class="columns is-mobile">
@ -24,7 +25,7 @@
<section class="block content">
<form name="create-list" method="post" action="{% url 'lists' %}" class="box hidden" id="create-list">
<header class="columns">
<h3 class="title column">Create list</h3>
<h3 class="title column">{% trans "Create list" %}</h3>
<div class="column is-narrow">
{% include 'snippets/toggle/toggle_button.html' with controls_text="create-list" label="close" class="delete" nonbutton=True %}
</div>

View file

@ -1,15 +1,16 @@
{% extends 'user/user_layout.html' %}
{% load i18n %}
{% block header %}
<div class="columns is-mobile">
<div class="column">
<h1 class="title">User profile</h1>
<h1 class="title">{% trans "User profile" %}</h1>
</div>
{% if is_self %}
<div class="column is-narrow">
<a href="/preferences/profile">
<span class="icon icon-pencil" title="Edit profile">
<span class="is-sr-only">Edit profile</span>
<span class="is-sr-only">{% trans "Edit profile" %}</span>
</span>
</a>
</div>
@ -20,12 +21,12 @@
{% block panel %}
{% if user.bookwyrm_user %}
<div class="block">
<h2 class="title">Shelves</h2>
<h2 class="title">{% trans "Shelves" %}</h2>
<div class="columns">
{% for shelf in shelves %}
<div class="column is-narrow">
<h3>{{ shelf.name }}
{% if shelf.size > 3 %}<small>(<a href="{{ shelf.local_path }}">See all {{ shelf.size }}</a>)</small>{% endif %}</h3>
{% if shelf.size > 3 %}<small>(<a href="{{ shelf.local_path }}">{% blocktrans with size=shelf.size %}See all {{ size }}{% endblocktrans %}</a>)</small>{% endif %}</h3>
<div class="is-mobile field is-grouped">
{% for book in shelf.books %}
<div class="control">
@ -38,7 +39,7 @@
</div>
{% endfor %}
</div>
<small><a href="{{ user.local_path }}/shelves">See all {{ shelf_count }} shelves</a></small>
<small><a href="{{ user.local_path }}/shelves">{% blocktrans %}See all {{ shelf_count }} shelves{% endblocktrans %}</a></small>
</div>
{% endif %}
@ -49,16 +50,17 @@
</div>
{% elif user == request.user %}
<div class="block">
<h2 class="title is-4"><a href="{{ user.local_path }}/goal/{% now 'Y' %}">Set a reading goal for {% now 'Y' %}</a></h2>
{% now 'Y' as year %}
<h2 class="title is-4"><a href="{{ user.local_path }}/goal/{{ year }}">{% blocktrans %}Set a reading goal for {{ year }}{% endblocktrans %}</a></h2>
</div>
{% endif %}
<div>
<div class="columns is-mobile">
<h2 class="title column">User Activity</h2>
<h2 class="title column">{% trans "User Activity" %}</h2>
<div class="column is-narrow">
<a class="icon icon-rss" target="_blank" href="{{ user.local_path }}/rss">
<span class="is-sr-only">RSS feed</span>
<span class="is-sr-only">{% trans "RSS feed" %}</span>
</a>
</div>
</div>
@ -69,7 +71,7 @@
{% endfor %}
{% if not activities %}
<div class="block">
<p>No activities yet!</a>
<p>{% trans "No activities yet!" %}</a>
</div>
{% endif %}

View file

@ -1,4 +1,5 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load humanize %}
{% load bookwyrm_tags %}
@ -10,35 +11,13 @@
{# user bio #}
<div class="block">
<div class="columns">
<div class="column is-narrow">
<div class="media">
<div class="media-left">
<a href="{{ user.local_path }}">
{% include 'snippets/avatar.html' with user=user large=True %}
</a>
</div>
<div class="media-content">
<p>{% if user.name %}{{ user.name }}{% else %}{{ user.localname }}{% endif %}</p>
<p><a href="{{ user.remote_id }}">{{ user.username }}</a></p>
<p>Joined {{ user.created_date | naturaltime }}</p>
<p>
<a href="{{ user.local_path }}/followers">{{ user.followers.count }} follower{{ user.followers.count | pluralize }}</a>,
<a href="{{ user.local_path }}/following">{{ user.following.count }} following</a>
</p>
</div>
</div>
<div class="column is-two-fifths">
{% include 'user/user_preview.html' with user=user %}
</div>
<div class="column">
<div class="column box has-background-white-bis content">
{% if user.summary %}
<div class="columns">
<div class="column is-narrow">
<span class="icon icon-quote-open"></span>
</div>
<div class="column">
<blockquote>{{ user.summary | to_markdown | safe }}</blockquote>
</div>
</div>
{{ user.summary | to_markdown | safe }}
{% endif %}
</div>
</div>
@ -48,7 +27,7 @@
{% if is_self and user.follower_requests.all %}
<div class="follow-requests">
<h2>Follow Requests</h2>
<h2>{% trans "Follow Requests" %}</h2>
{% for requester in user.follower_requests.all %}
<div class="row shrink">
<p>
@ -66,25 +45,25 @@
<ul>
{% url 'user-feed' user|username as url %}
<li{% if url == request.path or url == request.path|add:'/' %} class="is-active"{% endif %}>
<a href="{{ url }}">Activity</a>
<a href="{{ url }}">{% trans "Activity" %}</a>
</li>
{% if is_self or user.goal.exists %}
{% now 'Y' as year %}
{% url 'user-goal' user|username year as url %}
<li{% if url in request.path %} class="is-active"{% endif %}>
<a href="{{ url }}">Reading Goal</a>
<a href="{{ url }}">{% trans "Reading Goal" %}</a>
</li>
{% endif %}
{% if is_self or user.lists.exists %}
{% url 'user-lists' user|username as url %}
<li{% if url in request.path %} class="is-active"{% endif %}>
<a href="{{ url }}">Lists</a>
<a href="{{ url }}">{% trans "Lists" %}</a>
</li>
{% endif %}
{% if user.shelf_set.exists %}
{% url 'user-shelves' user|username as url %}
<li{% if url in request.path %} class="is-active"{% endif %}>
<a href="{{ url }}">Shelves</a>
<a href="{{ url }}">{% trans "Shelves" %}</a>
</li>
{% endif %}
</ul>

View file

@ -0,0 +1,20 @@
{% load i18n %}
{% load humanize %}
<div class="media block">
<div class="media-left">
<a href="{{ user.local_path }}">
{% include 'snippets/avatar.html' with user=user large=True %}
</a>
</div>
<div class="media-content">
<p>{% if user.name %}{{ user.name }}{% else %}{{ user.localname }}{% endif %}</p>
<p><a href="{{ user.remote_id }}">{{ user.username }}</a></p>
<p>{% blocktrans with date=user.created_date|naturaltime %}Joined {{ date }}{% endblocktrans %}</p>
<p>
<a href="{{ user.local_path }}/followers">{{ user.followers.count }} follower{{ user.followers.count | pluralize }}</a>,
<a href="{{ user.local_path }}/following">{{ user.following.count }} following</a>
</p>
</div>
</div>

View file

@ -0,0 +1,4 @@
Book Id Title Sort Character Primary Author Primary Author Role Secondary Author Secondary Author Roles Publication Date Review Rating Comment Private Comment Summary Media Physical Description Weight Height Thickness Length Dimensions Page Count LCCN Acquired Date Started Date Read Barcode BCID Tags Collections Languages Original Languages LC Classification ISBN ISBNs Subjects Dewey Decimal Dewey Wording Other Call Number Copies Source Entry Date From Where OCLC Work id Lending Patron Lending Status Lending Start Lending End
5498194 Marelle 1 Cortázar, Julio Gallimard (1979), Poche 1979 chef d'oeuvre 4.5 Marelle by Julio Cortázar (1979) Broché 590 p.; 7.24 inches 1.28 pounds 7.24 inches 1.26 inches 4.96 inches 7.24 x 4.96 x 1.26 inches 590 [2007-04-16] [2007-05-08] roman, espagnol, expérimental, bohème, philosophie Your library French Spanish PQ7797 .C7145 [2070291340] 2070291340, 9782070291342 Cortâazar, Julio. Rayuela 863 Literature > Spanish And Portuguese > Spanish fiction 1 Amazon.fr [2006-08-09] 57814
5015319 Le grand incendie de Londres: Récit, avec incises et bifurcations, 1985-1987 (Fiction & Cie) 1 Roubaud, Jacques Seuil (1989), Unknown Binding 1989 5 Le grand incendie de Londres: Récit, avec incises et bifurcations, 1985-1987 (Fiction & Cie) by Jacques Roubaud (1989) Broché 411 p.; 7.72 inches 0.88 pounds 7.72 inches 1.02 inches 5.43 inches 7.72 x 5.43 x 1.02 inches 411 Your library English PQ2678 .O77 [2020104725] 2020104725, 9782020104722 Autobiographical fiction|Roubaud, Jacques > Fiction 813 American And Canadian > Fiction > Literature 1 Amazon.com [2006-07-25] 478910
5015399 Le Maître et Marguerite 1 Boulgakov, Mikhaïl Pocket (1994), Poche 1994 Le Maître et Marguerite by Mikhaïl Boulgakov (1994) Broché 579 p.; 7.09 inches 0.66 pounds 7.09 inches 1.18 inches 4.33 inches 7.09 x 4.33 x 1.18 inches 579 Your library French PG3476 .B78 [2266062328] 2266062328, 9782266062329 Allegories|Bulgakov|Good and evil > Fiction|Humanities|Jerusalem > Fiction|Jesus Christ > Fiction|Literature|Mental illness > Fiction|Moscow (Russia) > Fiction|Novel|Pilate, Pontius, 1st cent. > Fiction|Political fiction|Russia > Fiction|Russian fiction|Russian publications (Form Entry)|Soviet Union > History > 1925-1953 > Fiction|literature 891.7342 1917-1945 > 1917-1991 (USSR) > Literature > Literature of other Indo-European languages > Other Languages > Russian > Russian Fiction 1 Amazon.fr [2006-07-25] 10151
1 Book Id Title Sort Character Primary Author Primary Author Role Secondary Author Secondary Author Roles Publication Date Review Rating Comment Private Comment Summary Media Physical Description Weight Height Thickness Length Dimensions Page Count LCCN Acquired Date Started Date Read Barcode BCID Tags Collections Languages Original Languages LC Classification ISBN ISBNs Subjects Dewey Decimal Dewey Wording Other Call Number Copies Source Entry Date From Where OCLC Work id Lending Patron Lending Status Lending Start Lending End
2 5498194 Marelle 1 Cortázar, Julio Gallimard (1979), Poche 1979 chef d'oeuvre 4.5 Marelle by Julio Cortázar (1979) Broché 590 p.; 7.24 inches 1.28 pounds 7.24 inches 1.26 inches 4.96 inches 7.24 x 4.96 x 1.26 inches 590 [2007-04-16] [2007-05-08] roman, espagnol, expérimental, bohème, philosophie Your library French Spanish PQ7797 .C7145 [2070291340] 2070291340, 9782070291342 Cortâazar, Julio. Rayuela 863 Literature > Spanish And Portuguese > Spanish fiction 1 Amazon.fr [2006-08-09] 57814
3 5015319 Le grand incendie de Londres: Récit, avec incises et bifurcations, 1985-1987 (Fiction & Cie) 1 Roubaud, Jacques Seuil (1989), Unknown Binding 1989 5 Le grand incendie de Londres: Récit, avec incises et bifurcations, 1985-1987 (Fiction & Cie) by Jacques Roubaud (1989) Broché 411 p.; 7.72 inches 0.88 pounds 7.72 inches 1.02 inches 5.43 inches 7.72 x 5.43 x 1.02 inches 411 Your library English PQ2678 .O77 [2020104725] 2020104725, 9782020104722 Autobiographical fiction|Roubaud, Jacques > Fiction 813 American And Canadian > Fiction > Literature 1 Amazon.com [2006-07-25] 478910
4 5015399 Le Maître et Marguerite 1 Boulgakov, Mikhaïl Pocket (1994), Poche 1994 Le Maître et Marguerite by Mikhaïl Boulgakov (1994) Broché 579 p.; 7.09 inches 0.66 pounds 7.09 inches 1.18 inches 4.33 inches 7.09 x 4.33 x 1.18 inches 579 Your library French PG3476 .B78 [2266062328] 2266062328, 9782266062329 Allegories|Bulgakov|Good and evil > Fiction|Humanities|Jerusalem > Fiction|Jesus Christ > Fiction|Literature|Mental illness > Fiction|Moscow (Russia) > Fiction|Novel|Pilate, Pontius, 1st cent. > Fiction|Political fiction|Russia > Fiction|Russian fiction|Russian publications (Form Entry)|Soviet Union > History > 1925-1953 > Fiction|literature 891.7342 1917-1945 > 1917-1991 (USSR) > Literature > Literature of other Indo-European languages > Other Languages > Russian > Russian Fiction 1 Amazon.fr [2006-07-25] 10151

View file

@ -7,16 +7,19 @@ from unittest.mock import patch
from django.test import TestCase
import responses
from bookwyrm import goodreads_import, models
from bookwyrm import models, importer
from bookwyrm.goodreads_import import GoodreadsImporter
from bookwyrm import importer
from bookwyrm.settings import DOMAIN
class GoodreadsImport(TestCase):
''' importing from goodreads csv '''
def setUp(self):
self.importer = GoodreadsImporter()
''' use a test csv '''
datafile = pathlib.Path(__file__).parent.joinpath(
'data/goodreads.csv')
self.csv = open(datafile, 'r')
self.csv = open(datafile, 'r', encoding=self.importer.encoding)
self.user = models.User.objects.create_user(
'mouse', 'mouse@mouse.mouse', 'password', local=True)
@ -41,7 +44,7 @@ class GoodreadsImport(TestCase):
def test_create_job(self):
''' creates the import job entry and checks csv '''
import_job = goodreads_import.create_job(
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)
@ -59,13 +62,13 @@ class GoodreadsImport(TestCase):
def test_create_retry_job(self):
''' trying again with items that didn't import '''
import_job = goodreads_import.create_job(
import_job = self.importer.create_job(
self.user, self.csv, False, 'unlisted')
import_items = models.ImportItem.objects.filter(
job=import_job
).all()[:2]
retry = goodreads_import.create_retry_job(
retry = self.importer.create_retry_job(
self.user, import_job, import_items)
self.assertNotEqual(import_job, retry)
self.assertEqual(retry.user, self.user)
@ -82,13 +85,13 @@ class GoodreadsImport(TestCase):
def test_start_import(self):
''' begin loading books '''
import_job = goodreads_import.create_job(
import_job = self.importer.create_job(
self.user, self.csv, False, 'unlisted')
MockTask = namedtuple('Task', ('id'))
mock_task = MockTask(7)
with patch('bookwyrm.goodreads_import.import_data.delay') as start:
with patch('bookwyrm.importer.import_data.delay') as start:
start.return_value = mock_task
goodreads_import.start_import(import_job)
self.importer.start_import(import_job)
import_job.refresh_from_db()
self.assertEqual(import_job.task_id, '7')
@ -96,7 +99,7 @@ class GoodreadsImport(TestCase):
@responses.activate
def test_import_data(self):
''' resolve entry '''
import_job = goodreads_import.create_job(
import_job = self.importer.create_job(
self.user, self.csv, False, 'unlisted')
book = models.Edition.objects.create(title='Test Book')
@ -104,8 +107,8 @@ class GoodreadsImport(TestCase):
'bookwyrm.models.import_job.ImportItem.get_book_from_isbn'
) as resolve:
resolve.return_value = book
with patch('bookwyrm.goodreads_import.handle_imported_book'):
goodreads_import.import_data(import_job.id)
with patch('bookwyrm.importer.handle_imported_book'):
importer.import_data(self.importer.service, import_job.id)
import_item = models.ImportItem.objects.get(job=import_job, index=0)
self.assertEqual(import_item.book.id, book.id)
@ -120,13 +123,14 @@ class GoodreadsImport(TestCase):
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
csv_file = open(datafile, 'r')
for index, entry in enumerate(list(csv.DictReader(csv_file))):
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=index, data=entry, book=self.book)
break
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
goodreads_import.handle_imported_book(
self.user, import_item, False, 'public')
importer.handle_imported_book(
self.importer.service, self.user, import_item, False, 'public')
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)
@ -153,13 +157,14 @@ class GoodreadsImport(TestCase):
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
csv_file = open(datafile, 'r')
for index, entry in enumerate(list(csv.DictReader(csv_file))):
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=index, data=entry, book=self.book)
break
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
goodreads_import.handle_imported_book(
self.user, import_item, False, 'public')
importer.handle_imported_book(
self.importer.service, self.user, import_item, False, 'public')
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)
@ -182,15 +187,16 @@ class GoodreadsImport(TestCase):
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
csv_file = open(datafile, 'r')
for index, entry in enumerate(list(csv.DictReader(csv_file))):
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=index, data=entry, book=self.book)
break
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
goodreads_import.handle_imported_book(
self.user, import_item, False, 'public')
goodreads_import.handle_imported_book(
self.user, import_item, False, 'public')
importer.handle_imported_book(
self.importer.service, self.user, import_item, False, 'public')
importer.handle_imported_book(
self.importer.service, self.user, import_item, False, 'public')
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)
@ -212,12 +218,13 @@ class GoodreadsImport(TestCase):
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.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'):
goodreads_import.handle_imported_book(
self.user, import_item, True, 'unlisted')
importer.handle_imported_book(
self.importer.service, self.user, import_item, True, 'unlisted')
review = models.Review.objects.get(book=self.book, user=self.user)
self.assertEqual(review.content, 'mixed feelings')
self.assertEqual(review.rating, 2)
@ -233,12 +240,13 @@ class GoodreadsImport(TestCase):
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.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'):
goodreads_import.handle_imported_book(
self.user, import_item, False, 'unlisted')
importer.handle_imported_book(
self.importer.service, self.user, import_item, False, 'unlisted')
self.assertFalse(models.Review.objects.filter(
book=self.book, user=self.user
).exists())

View file

@ -0,0 +1,240 @@
''' testing import '''
from collections import namedtuple
import csv
import pathlib
from unittest.mock import patch
from django.test import TestCase
import responses
from bookwyrm import models, importer
from bookwyrm.librarything_import import LibrarythingImporter
from bookwyrm.settings import DOMAIN
class LibrarythingImport(TestCase):
''' importing from librarything tsv '''
def setUp(self):
self.importer = LibrarythingImporter()
''' use a test tsv '''
datafile = pathlib.Path(__file__).parent.joinpath(
'data/librarything.tsv')
# Librarything generates latin encoded exports...
self.csv = open(datafile, 'r', encoding=self.importer.encoding)
self.user = models.User.objects.create_user(
'mmai', 'mmai@mmai.mmai', 'password', local=True)
models.Connector.objects.create(
identifier=DOMAIN,
name='Local',
local=True,
connector_file='self_connector',
base_url='https://%s' % DOMAIN,
books_url='https://%s/book' % DOMAIN,
covers_url='https://%s/images/covers' % DOMAIN,
search_url='https://%s/search?q=' % DOMAIN,
priority=1,
)
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=work
)
def test_create_job(self):
''' 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)
self.assertEqual(import_job.privacy, 'public')
import_items = models.ImportItem.objects.filter(job=import_job).all()
self.assertEqual(len(import_items), 3)
self.assertEqual(import_items[0].index, 0)
self.assertEqual(import_items[0].data['Book Id'], '5498194')
self.assertEqual(import_items[1].index, 1)
self.assertEqual(import_items[1].data['Book Id'], '5015319')
self.assertEqual(import_items[2].index, 2)
self.assertEqual(import_items[2].data['Book Id'], '5015399')
def test_create_retry_job(self):
''' 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]
retry = self.importer.create_retry_job(
self.user, import_job, import_items)
self.assertNotEqual(import_job, retry)
self.assertEqual(retry.user, self.user)
self.assertEqual(retry.include_reviews, False)
self.assertEqual(retry.privacy, 'unlisted')
retry_items = models.ImportItem.objects.filter(job=retry).all()
self.assertEqual(len(retry_items), 2)
self.assertEqual(retry_items[0].index, 0)
self.assertEqual(import_items[0].data['Book Id'], '5498194')
self.assertEqual(retry_items[1].index, 1)
self.assertEqual(retry_items[1].data['Book Id'], '5015319')
@responses.activate
def test_import_data(self):
''' resolve entry '''
import_job = self.importer.create_job(
self.user, self.csv, False, 'unlisted')
book = models.Edition.objects.create(title='Test Book')
with patch(
'bookwyrm.models.import_job.ImportItem.get_book_from_isbn'
) as resolve:
resolve.return_value = book
with patch('bookwyrm.importer.handle_imported_book'):
importer.import_data(self.importer.service, import_job.id)
import_item = models.ImportItem.objects.get(job=import_job, index=0)
self.assertEqual(import_item.book.id, book.id)
def test_handle_imported_book(self):
''' librarything import added a book, this adds related connections '''
shelf = self.user.shelf_set.filter(identifier='read').first()
self.assertIsNone(shelf.books.first())
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)
for index, entry in enumerate(list(csv.DictReader(csv_file, delimiter=self.importer.delimiter))):
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=index, data=entry, book=self.book)
break
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
importer.handle_imported_book(
self.importer.service, self.user, import_item, False, 'public')
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)
readthrough = models.ReadThrough.objects.get(user=self.user)
self.assertEqual(readthrough.book, self.book)
# I can't remember how to create dates and I don't want to look it up.
self.assertEqual(readthrough.start_date.year, 2007)
self.assertEqual(readthrough.start_date.month, 4)
self.assertEqual(readthrough.start_date.day, 16)
self.assertEqual(readthrough.finish_date.year, 2007)
self.assertEqual(readthrough.finish_date.month, 5)
self.assertEqual(readthrough.finish_date.day, 8)
def test_handle_imported_book_already_shelved(self):
''' 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)
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)
for index, entry in enumerate(list(csv.DictReader(csv_file, delimiter=self.importer.delimiter))):
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=index, data=entry, book=self.book)
break
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
importer.handle_imported_book(
self.importer.service, self.user, import_item, False, 'public')
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)
self.assertIsNone(
self.user.shelf_set.get(identifier='read').books.first())
readthrough = models.ReadThrough.objects.get(user=self.user)
self.assertEqual(readthrough.book, self.book)
self.assertEqual(readthrough.start_date.year, 2007)
self.assertEqual(readthrough.start_date.month, 4)
self.assertEqual(readthrough.start_date.day, 16)
self.assertEqual(readthrough.finish_date.year, 2007)
self.assertEqual(readthrough.finish_date.month, 5)
self.assertEqual(readthrough.finish_date.day, 8)
def test_handle_import_twice(self):
''' 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')
csv_file = open(datafile, 'r', encoding=self.importer.encoding)
for index, entry in enumerate(list(csv.DictReader(csv_file, delimiter=self.importer.delimiter))):
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=index, data=entry, book=self.book)
break
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
importer.handle_imported_book(
self.importer.service, self.user, import_item, False, 'public')
importer.handle_imported_book(
self.importer.service, self.user, import_item, False, 'public')
shelf.refresh_from_db()
self.assertEqual(shelf.books.first(), self.book)
readthrough = models.ReadThrough.objects.get(user=self.user)
self.assertEqual(readthrough.book, self.book)
# I can't remember how to create dates and I don't want to look it up.
self.assertEqual(readthrough.start_date.year, 2007)
self.assertEqual(readthrough.start_date.month, 4)
self.assertEqual(readthrough.start_date.day, 16)
self.assertEqual(readthrough.finish_date.year, 2007)
self.assertEqual(readthrough.finish_date.month, 5)
self.assertEqual(readthrough.finish_date.day, 8)
def test_handle_imported_book_review(self):
''' 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)
entry = list(csv.DictReader(csv_file, delimiter=self.importer.delimiter))[0]
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'):
importer.handle_imported_book(
self.importer.service, self.user, import_item, True, 'unlisted')
review = models.Review.objects.get(book=self.book, user=self.user)
self.assertEqual(review.content, 'chef d\'oeuvre')
self.assertEqual(review.rating, 5)
self.assertEqual(review.published_date.year, 2007)
self.assertEqual(review.published_date.month, 5)
self.assertEqual(review.published_date.day, 8)
self.assertEqual(review.privacy, 'unlisted')
def test_handle_imported_book_reviews_disabled(self):
''' 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)
entry = list(csv.DictReader(csv_file, delimiter=self.importer.delimiter))[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'):
importer.handle_imported_book(
self.importer.service, self.user, import_item, False, 'unlisted')
self.assertFalse(models.Review.objects.filter(
book=self.book, user=self.user
).exists())

View file

@ -9,7 +9,7 @@ from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import forms, goodreads_import, models
from bookwyrm import forms, goodreads_import, librarything_import, models
from bookwyrm.tasks import app
# pylint: disable= no-self-use
@ -31,18 +31,29 @@ class Import(View):
if form.is_valid():
include_reviews = request.POST.get('include_reviews') == 'on'
privacy = request.POST.get('privacy')
source = request.POST.get('source')
importer = None
if source == 'LibraryThing':
importer = librarything_import.LibrarythingImporter()
else:
# Default : GoodReads
importer = goodreads_import.GoodreadsImporter()
try:
job = goodreads_import.create_job(
job = importer.create_job(
request.user,
TextIOWrapper(
request.FILES['csv_file'],
encoding=request.encoding),
encoding=importer.encoding),
include_reviews,
privacy,
)
except (UnicodeDecodeError, ValueError):
return HttpResponseBadRequest('Not a valid csv file')
goodreads_import.start_import(job)
importer.start_import(job)
return redirect('/import/%d' % job.id)
return HttpResponseBadRequest()

Some files were not shown because too many files have changed in this diff Show more