mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-25 11:01:12 +00:00
Merge branch 'main' into bw-dev-npm-fix
This commit is contained in:
commit
51f445bc72
220 changed files with 17881 additions and 4815 deletions
3
.github/workflows/pylint.yml
vendored
3
.github/workflows/pylint.yml
vendored
|
@ -21,8 +21,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install pylint
|
|
||||||
- name: Analysing the code with pylint
|
- name: Analysing the code with pylint
|
||||||
run: |
|
run: |
|
||||||
pylint bookwyrm/ --ignore=migrations,tests --disable=E1101,E1135,E1136,R0903,R0901,R0902,W0707,W0511,W0406,R0401,R0801
|
pylint bookwyrm/
|
||||||
|
|
||||||
|
|
6
.pylintrc
Normal file
6
.pylintrc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[MAIN]
|
||||||
|
ignore=migrations
|
||||||
|
load-plugins=pylint.extensions.no_self_use
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
disable=E1101,E1135,E1136,R0903,R0901,R0902,W0707,W0511,W0406,R0401,R0801,C3001
|
|
@ -6,6 +6,7 @@ RUN mkdir /app /app/static /app/images
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y gettext libgettextpo-dev tidy && apt-get clean
|
||||||
|
|
||||||
COPY requirements.txt /app/
|
COPY requirements.txt /app/
|
||||||
RUN pip install -r requirements.txt --no-cache-dir
|
RUN pip install -r requirements.txt --no-cache-dir
|
||||||
RUN apt-get update && apt-get install -y gettext libgettextpo-dev tidy && apt-get clean
|
|
||||||
|
|
18
README.md
18
README.md
|
@ -9,21 +9,18 @@ Social reading and reviewing, decentralized with ActivityPub
|
||||||
- [What it is and isn't](#what-it-is-and-isnt)
|
- [What it is and isn't](#what-it-is-and-isnt)
|
||||||
- [The role of federation](#the-role-of-federation)
|
- [The role of federation](#the-role-of-federation)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Book data](#book-data)
|
- [Set up BookWyrm](#set-up-bookwyrm)
|
||||||
- [Set up Bookwyrm](#set-up-bookwyrm)
|
|
||||||
|
|
||||||
## Joining BookWyrm
|
## Joining BookWyrm
|
||||||
BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://joinbookwyrm.com/instances/) list.
|
If you'd like to join an instance, you can check out the [instances](https://joinbookwyrm.com/instances/) list.
|
||||||
|
|
||||||
You can request an invite by entering your email address at https://bookwyrm.social.
|
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
See [contributing](https://docs.joinbookwyrm.com/how-to-contribute.html) for code, translation or monetary contributions.
|
See [contributing](https://docs.joinbookwyrm.com/contributing.html) for code, translation or monetary contributions.
|
||||||
|
|
||||||
## About BookWyrm
|
## About BookWyrm
|
||||||
### What it is and isn't
|
### 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 data-source 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
|
### 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.
|
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.
|
||||||
|
@ -78,8 +75,5 @@ Deployment
|
||||||
- [Nginx](https://nginx.org/en/) HTTP server
|
- [Nginx](https://nginx.org/en/) HTTP server
|
||||||
|
|
||||||
|
|
||||||
## Book data
|
## Set up BookWyrm
|
||||||
The application is set up to share book and author data between instances, and get book data from arbitrary outside sources. Right now, the only connector is to OpenLibrary, but other connectors could be written.
|
The [documentation website](https://docs.joinbookwyrm.com/) has instruction on how to set up BookWyrm in a [developer environment](https://docs.joinbookwyrm.com/install-dev.html) or [production](https://docs.joinbookwyrm.com/install-prod.html).
|
||||||
|
|
||||||
## Set up Bookwyrm
|
|
||||||
The [documentation website](https://docs.joinbookwyrm.com/) has instruction on how to set up Bookwyrm in a [developer environment](https://docs.joinbookwyrm.com/developer-environment.html) or [production](https://docs.joinbookwyrm.com/installing-in-production.html).
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
""" basics for an activitypub serializer """
|
""" basics for an activitypub serializer """
|
||||||
from dataclasses import dataclass, fields, MISSING
|
from dataclasses import dataclass, fields, MISSING
|
||||||
from json import JSONEncoder
|
from json import JSONEncoder
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import IntegrityError, transaction
|
from django.db import IntegrityError, transaction
|
||||||
|
@ -8,6 +9,8 @@ from django.db import IntegrityError, transaction
|
||||||
from bookwyrm.connectors import ConnectorException, get_data
|
from bookwyrm.connectors import ConnectorException, get_data
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ActivitySerializerError(ValueError):
|
class ActivitySerializerError(ValueError):
|
||||||
"""routine problems serializing activitypub json"""
|
"""routine problems serializing activitypub json"""
|
||||||
|
@ -39,12 +42,12 @@ def naive_parse(activity_objects, activity_json, serializer=None):
|
||||||
activity_json["type"] = "PublicKey"
|
activity_json["type"] = "PublicKey"
|
||||||
|
|
||||||
activity_type = activity_json.get("type")
|
activity_type = activity_json.get("type")
|
||||||
|
if activity_type in ["Question", "Article"]:
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
serializer = activity_objects[activity_type]
|
serializer = activity_objects[activity_type]
|
||||||
except KeyError as err:
|
except KeyError as err:
|
||||||
# we know this exists and that we can't handle it
|
# we know this exists and that we can't handle it
|
||||||
if activity_type in ["Question"]:
|
|
||||||
return None
|
|
||||||
raise ActivitySerializerError(err)
|
raise ActivitySerializerError(err)
|
||||||
|
|
||||||
return serializer(activity_objects=activity_objects, **activity_json)
|
return serializer(activity_objects=activity_objects, **activity_json)
|
||||||
|
@ -65,7 +68,7 @@ class ActivityObject:
|
||||||
try:
|
try:
|
||||||
value = kwargs[field.name]
|
value = kwargs[field.name]
|
||||||
if value in (None, MISSING, {}):
|
if value in (None, MISSING, {}):
|
||||||
raise KeyError()
|
raise KeyError("Missing required field", field.name)
|
||||||
try:
|
try:
|
||||||
is_subclass = issubclass(field.type, ActivityObject)
|
is_subclass = issubclass(field.type, ActivityObject)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
@ -268,9 +271,9 @@ def resolve_remote_id(
|
||||||
try:
|
try:
|
||||||
data = get_data(remote_id)
|
data = get_data(remote_id)
|
||||||
except ConnectorException:
|
except ConnectorException:
|
||||||
raise ActivitySerializerError(
|
logger.exception("Could not connect to host for remote_id: %s", remote_id)
|
||||||
f"Could not connect to host for remote_id: {remote_id}"
|
return None
|
||||||
)
|
|
||||||
# determine the model implicitly, if not provided
|
# determine the model implicitly, if not provided
|
||||||
# or if it's a model with subclasses like Status, check again
|
# or if it's a model with subclasses like Status, check again
|
||||||
if not model or hasattr(model.objects, "select_subclasses"):
|
if not model or hasattr(model.objects, "select_subclasses"):
|
||||||
|
|
|
@ -298,8 +298,9 @@ def add_status_on_create_command(sender, instance, created):
|
||||||
priority = HIGH
|
priority = HIGH
|
||||||
# check if this is an old status, de-prioritize if so
|
# check if this is an old status, de-prioritize if so
|
||||||
# (this will happen if federation is very slow, or, more expectedly, on csv import)
|
# (this will happen if federation is very slow, or, more expectedly, on csv import)
|
||||||
one_day = 60 * 60 * 24
|
if instance.published_date < timezone.now() - timedelta(
|
||||||
if (instance.created_date - instance.published_date).seconds > one_day:
|
days=1
|
||||||
|
) or instance.created_date < instance.published_date - timedelta(days=1):
|
||||||
priority = LOW
|
priority = LOW
|
||||||
|
|
||||||
add_status_task.apply_async(
|
add_status_task.apply_async(
|
||||||
|
|
|
@ -148,8 +148,8 @@ class SearchResult:
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
# pylint: disable=consider-using-f-string
|
# pylint: disable=consider-using-f-string
|
||||||
return "<SearchResult key={!r} title={!r} author={!r}>".format(
|
return "<SearchResult key={!r} title={!r} author={!r} confidence={!r}>".format(
|
||||||
self.key, self.title, self.author
|
self.key, self.title, self.author, self.confidence
|
||||||
)
|
)
|
||||||
|
|
||||||
def json(self):
|
def json(self):
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
""" functionality outline for a book data connector """
|
""" functionality outline for a book data connector """
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
import imghdr
|
import imghdr
|
||||||
import ipaddress
|
|
||||||
import logging
|
import logging
|
||||||
from urllib.parse import urlparse
|
import re
|
||||||
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
@ -11,7 +10,7 @@ import requests
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
from bookwyrm import activitypub, models, settings
|
from bookwyrm import activitypub, models, settings
|
||||||
from .connector_manager import load_more_data, ConnectorException
|
from .connector_manager import load_more_data, ConnectorException, raise_not_valid_url
|
||||||
from .format_mappings import format_mappings
|
from .format_mappings import format_mappings
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,62 +38,34 @@ class AbstractMinimalConnector(ABC):
|
||||||
for field in self_fields:
|
for field in self_fields:
|
||||||
setattr(self, field, getattr(info, field))
|
setattr(self, field, getattr(info, field))
|
||||||
|
|
||||||
def search(self, query, min_confidence=None, timeout=settings.QUERY_TIMEOUT):
|
def get_search_url(self, query):
|
||||||
"""free text search"""
|
"""format the query url"""
|
||||||
params = {}
|
# Check if the query resembles an ISBN
|
||||||
if min_confidence:
|
if maybe_isbn(query) and self.isbn_search_url and self.isbn_search_url != "":
|
||||||
params["min_confidence"] = min_confidence
|
return f"{self.isbn_search_url}{query}"
|
||||||
|
|
||||||
data = self.get_search_data(
|
# NOTE: previously, we tried searching isbn and if that produces no results,
|
||||||
f"{self.search_url}{query}",
|
# searched as free text. This, instead, only searches isbn if it's isbn-y
|
||||||
params=params,
|
return f"{self.search_url}{query}"
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
results = []
|
|
||||||
|
|
||||||
for doc in self.parse_search_data(data)[:10]:
|
def process_search_response(self, query, data, min_confidence):
|
||||||
results.append(self.format_search_result(doc))
|
"""Format the search results based on the formt of the query"""
|
||||||
return results
|
if maybe_isbn(query):
|
||||||
|
return list(self.parse_isbn_search_data(data))[:10]
|
||||||
def isbn_search(self, query, timeout=settings.QUERY_TIMEOUT):
|
return list(self.parse_search_data(data, min_confidence))[:10]
|
||||||
"""isbn search"""
|
|
||||||
params = {}
|
|
||||||
data = self.get_search_data(
|
|
||||||
f"{self.isbn_search_url}{query}",
|
|
||||||
params=params,
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
results = []
|
|
||||||
|
|
||||||
# this shouldn't be returning mutliple results, but just in case
|
|
||||||
for doc in self.parse_isbn_search_data(data)[:10]:
|
|
||||||
results.append(self.format_isbn_search_result(doc))
|
|
||||||
return results
|
|
||||||
|
|
||||||
def get_search_data(self, remote_id, **kwargs): # pylint: disable=no-self-use
|
|
||||||
"""this allows connectors to override the default behavior"""
|
|
||||||
return get_data(remote_id, **kwargs)
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_or_create_book(self, remote_id):
|
def get_or_create_book(self, remote_id):
|
||||||
"""pull up a book record by whatever means possible"""
|
"""pull up a book record by whatever means possible"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def parse_search_data(self, data):
|
def parse_search_data(self, data, min_confidence):
|
||||||
"""turn the result json from a search into a list"""
|
"""turn the result json from a search into a list"""
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def format_search_result(self, search_result):
|
|
||||||
"""create a SearchResult obj from json"""
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def parse_isbn_search_data(self, data):
|
def parse_isbn_search_data(self, data):
|
||||||
"""turn the result json from a search into a list"""
|
"""turn the result json from a search into a list"""
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def format_isbn_search_result(self, search_result):
|
|
||||||
"""create a SearchResult obj from json"""
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractConnector(AbstractMinimalConnector):
|
class AbstractConnector(AbstractMinimalConnector):
|
||||||
"""generic book data connector"""
|
"""generic book data connector"""
|
||||||
|
@ -254,9 +225,6 @@ def get_data(url, params=None, timeout=10):
|
||||||
# check if the url is blocked
|
# check if the url is blocked
|
||||||
raise_not_valid_url(url)
|
raise_not_valid_url(url)
|
||||||
|
|
||||||
if models.FederatedServer.is_blocked(url):
|
|
||||||
raise ConnectorException(f"Attempting to load data from blocked url: {url}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
url,
|
url,
|
||||||
|
@ -311,20 +279,6 @@ def get_image(url, timeout=10):
|
||||||
return image_content, extension
|
return image_content, extension
|
||||||
|
|
||||||
|
|
||||||
def raise_not_valid_url(url):
|
|
||||||
"""do some basic reality checks on the url"""
|
|
||||||
parsed = urlparse(url)
|
|
||||||
if not parsed.scheme in ["http", "https"]:
|
|
||||||
raise ConnectorException("Invalid scheme: ", url)
|
|
||||||
|
|
||||||
try:
|
|
||||||
ipaddress.ip_address(parsed.netloc)
|
|
||||||
raise ConnectorException("Provided url is an IP address: ", url)
|
|
||||||
except ValueError:
|
|
||||||
# it's not an IP address, which is good
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Mapping:
|
class Mapping:
|
||||||
"""associate a local database field with a field in an external dataset"""
|
"""associate a local database field with a field in an external dataset"""
|
||||||
|
|
||||||
|
@ -366,3 +320,9 @@ def unique_physical_format(format_text):
|
||||||
# try a direct match, so saving this would be redundant
|
# try a direct match, so saving this would be redundant
|
||||||
return None
|
return None
|
||||||
return format_text
|
return format_text
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_isbn(query):
|
||||||
|
"""check if a query looks like an isbn"""
|
||||||
|
isbn = re.sub(r"[\W_]", "", query) # removes filler characters
|
||||||
|
return len(isbn) in [10, 13] # ISBN10 or ISBN13
|
||||||
|
|
|
@ -10,15 +10,12 @@ class Connector(AbstractMinimalConnector):
|
||||||
def get_or_create_book(self, remote_id):
|
def get_or_create_book(self, remote_id):
|
||||||
return activitypub.resolve_remote_id(remote_id, model=models.Edition)
|
return activitypub.resolve_remote_id(remote_id, model=models.Edition)
|
||||||
|
|
||||||
def parse_search_data(self, data):
|
def parse_search_data(self, data, min_confidence):
|
||||||
return data
|
for search_result in data:
|
||||||
|
search_result["connector"] = self
|
||||||
def format_search_result(self, search_result):
|
yield SearchResult(**search_result)
|
||||||
search_result["connector"] = self
|
|
||||||
return SearchResult(**search_result)
|
|
||||||
|
|
||||||
def parse_isbn_search_data(self, data):
|
def parse_isbn_search_data(self, data):
|
||||||
return data
|
for search_result in data:
|
||||||
|
search_result["connector"] = self
|
||||||
def format_isbn_search_result(self, search_result):
|
yield SearchResult(**search_result)
|
||||||
return self.format_search_result(search_result)
|
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
""" interface with whatever connectors the app has """
|
""" interface with whatever connectors the app has """
|
||||||
from datetime import datetime
|
import asyncio
|
||||||
import importlib
|
import importlib
|
||||||
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
|
|
||||||
from requests import HTTPError
|
from requests import HTTPError
|
||||||
|
|
||||||
from bookwyrm import book_search, models
|
from bookwyrm import book_search, models
|
||||||
from bookwyrm.settings import SEARCH_TIMEOUT
|
from bookwyrm.settings import SEARCH_TIMEOUT, USER_AGENT
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -21,53 +22,85 @@ class ConnectorException(HTTPError):
|
||||||
"""when the connector can't do what was asked"""
|
"""when the connector can't do what was asked"""
|
||||||
|
|
||||||
|
|
||||||
|
async def get_results(session, url, min_confidence, query, connector):
|
||||||
|
"""try this specific connector"""
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
headers = {
|
||||||
|
"Accept": (
|
||||||
|
'application/json, application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'
|
||||||
|
),
|
||||||
|
"User-Agent": USER_AGENT,
|
||||||
|
}
|
||||||
|
params = {"min_confidence": min_confidence}
|
||||||
|
try:
|
||||||
|
async with session.get(url, headers=headers, params=params) as response:
|
||||||
|
if not response.ok:
|
||||||
|
logger.info("Unable to connect to %s: %s", url, response.reason)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw_data = await response.json()
|
||||||
|
except aiohttp.client_exceptions.ContentTypeError as err:
|
||||||
|
logger.exception(err)
|
||||||
|
return
|
||||||
|
|
||||||
|
return {
|
||||||
|
"connector": connector,
|
||||||
|
"results": connector.process_search_response(
|
||||||
|
query, raw_data, min_confidence
|
||||||
|
),
|
||||||
|
}
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.info("Connection timed out for url: %s", url)
|
||||||
|
except aiohttp.ClientError as err:
|
||||||
|
logger.exception(err)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_connector_search(query, items, min_confidence):
|
||||||
|
"""Try a number of requests simultaneously"""
|
||||||
|
timeout = aiohttp.ClientTimeout(total=SEARCH_TIMEOUT)
|
||||||
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
|
tasks = []
|
||||||
|
for url, connector in items:
|
||||||
|
tasks.append(
|
||||||
|
asyncio.ensure_future(
|
||||||
|
get_results(session, url, min_confidence, query, connector)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
results = await asyncio.gather(*tasks)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def search(query, min_confidence=0.1, return_first=False):
|
def search(query, min_confidence=0.1, return_first=False):
|
||||||
"""find books based on arbitary keywords"""
|
"""find books based on arbitary keywords"""
|
||||||
if not query:
|
if not query:
|
||||||
return []
|
return []
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
# Have we got a ISBN ?
|
items = []
|
||||||
isbn = re.sub(r"[\W_]", "", query)
|
|
||||||
maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13
|
|
||||||
|
|
||||||
start_time = datetime.now()
|
|
||||||
for connector in get_connectors():
|
for connector in get_connectors():
|
||||||
result_set = None
|
# get the search url from the connector before sending
|
||||||
if maybe_isbn and connector.isbn_search_url and connector.isbn_search_url != "":
|
url = connector.get_search_url(query)
|
||||||
# Search on ISBN
|
try:
|
||||||
try:
|
raise_not_valid_url(url)
|
||||||
result_set = connector.isbn_search(isbn)
|
except ConnectorException:
|
||||||
except Exception as err: # pylint: disable=broad-except
|
# if this URL is invalid we should skip it and move on
|
||||||
logger.info(err)
|
logger.info("Request denied to blocked domain: %s", url)
|
||||||
# if this fails, we can still try regular search
|
continue
|
||||||
|
items.append((url, connector))
|
||||||
|
|
||||||
# if no isbn search results, we fallback to generic search
|
# load as many results as we can
|
||||||
if not result_set:
|
results = asyncio.run(async_connector_search(query, items, min_confidence))
|
||||||
try:
|
results = [r for r in results if r]
|
||||||
result_set = connector.search(query, min_confidence=min_confidence)
|
|
||||||
except Exception as err: # pylint: disable=broad-except
|
|
||||||
# we don't want *any* error to crash the whole search page
|
|
||||||
logger.info(err)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if return_first and result_set:
|
|
||||||
# if we found anything, return it
|
|
||||||
return result_set[0]
|
|
||||||
|
|
||||||
if result_set:
|
|
||||||
results.append(
|
|
||||||
{
|
|
||||||
"connector": connector,
|
|
||||||
"results": result_set,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (datetime.now() - start_time).seconds >= SEARCH_TIMEOUT:
|
|
||||||
break
|
|
||||||
|
|
||||||
if return_first:
|
if return_first:
|
||||||
return None
|
# find the best result from all the responses and return that
|
||||||
|
all_results = [r for con in results for r in con["results"]]
|
||||||
|
all_results = sorted(all_results, key=lambda r: r.confidence, reverse=True)
|
||||||
|
return all_results[0] if all_results else None
|
||||||
|
|
||||||
|
# failed requests will return None, so filter those out
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@ -133,3 +166,20 @@ def create_connector(sender, instance, created, *args, **kwargs):
|
||||||
"""create a connector to an external bookwyrm server"""
|
"""create a connector to an external bookwyrm server"""
|
||||||
if instance.application_type == "bookwyrm":
|
if instance.application_type == "bookwyrm":
|
||||||
get_or_create_connector(f"https://{instance.server_name}")
|
get_or_create_connector(f"https://{instance.server_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def raise_not_valid_url(url):
|
||||||
|
"""do some basic reality checks on the url"""
|
||||||
|
parsed = urlparse(url)
|
||||||
|
if not parsed.scheme in ["http", "https"]:
|
||||||
|
raise ConnectorException("Invalid scheme: ", url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ipaddress.ip_address(parsed.netloc)
|
||||||
|
raise ConnectorException("Provided url is an IP address: ", url)
|
||||||
|
except ValueError:
|
||||||
|
# it's not an IP address, which is good
|
||||||
|
pass
|
||||||
|
|
||||||
|
if models.FederatedServer.is_blocked(url):
|
||||||
|
raise ConnectorException(f"Attempting to load data from blocked url: {url}")
|
||||||
|
|
|
@ -77,53 +77,42 @@ class Connector(AbstractConnector):
|
||||||
**{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks", "type"]},
|
**{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks", "type"]},
|
||||||
}
|
}
|
||||||
|
|
||||||
def search(self, query, min_confidence=None): # pylint: disable=arguments-differ
|
def parse_search_data(self, data, min_confidence):
|
||||||
"""overrides default search function with confidence ranking"""
|
for search_result in data.get("results", []):
|
||||||
results = super().search(query)
|
images = search_result.get("image")
|
||||||
if min_confidence:
|
cover = f"{self.covers_url}/img/entities/{images[0]}" if images else None
|
||||||
# filter the search results after the fact
|
# a deeply messy translation of inventaire's scores
|
||||||
return [r for r in results if r.confidence >= min_confidence]
|
confidence = float(search_result.get("_score", 0.1))
|
||||||
return results
|
confidence = 0.1 if confidence < 150 else 0.999
|
||||||
|
if confidence < min_confidence:
|
||||||
def parse_search_data(self, data):
|
continue
|
||||||
return data.get("results")
|
yield SearchResult(
|
||||||
|
title=search_result.get("label"),
|
||||||
def format_search_result(self, search_result):
|
key=self.get_remote_id(search_result.get("uri")),
|
||||||
images = search_result.get("image")
|
author=search_result.get("description"),
|
||||||
cover = f"{self.covers_url}/img/entities/{images[0]}" if images else None
|
view_link=f"{self.base_url}/entity/{search_result.get('uri')}",
|
||||||
# a deeply messy translation of inventaire's scores
|
cover=cover,
|
||||||
confidence = float(search_result.get("_score", 0.1))
|
confidence=confidence,
|
||||||
confidence = 0.1 if confidence < 150 else 0.999
|
connector=self,
|
||||||
return SearchResult(
|
)
|
||||||
title=search_result.get("label"),
|
|
||||||
key=self.get_remote_id(search_result.get("uri")),
|
|
||||||
author=search_result.get("description"),
|
|
||||||
view_link=f"{self.base_url}/entity/{search_result.get('uri')}",
|
|
||||||
cover=cover,
|
|
||||||
confidence=confidence,
|
|
||||||
connector=self,
|
|
||||||
)
|
|
||||||
|
|
||||||
def parse_isbn_search_data(self, data):
|
def parse_isbn_search_data(self, data):
|
||||||
"""got some daaaata"""
|
"""got some daaaata"""
|
||||||
results = data.get("entities")
|
results = data.get("entities")
|
||||||
if not results:
|
if not results:
|
||||||
return []
|
return
|
||||||
return list(results.values())
|
for search_result in list(results.values()):
|
||||||
|
title = search_result.get("claims", {}).get("wdt:P1476", [])
|
||||||
def format_isbn_search_result(self, search_result):
|
if not title:
|
||||||
"""totally different format than a regular search result"""
|
continue
|
||||||
title = search_result.get("claims", {}).get("wdt:P1476", [])
|
yield SearchResult(
|
||||||
if not title:
|
title=title[0],
|
||||||
return None
|
key=self.get_remote_id(search_result.get("uri")),
|
||||||
return SearchResult(
|
author=search_result.get("description"),
|
||||||
title=title[0],
|
view_link=f"{self.base_url}/entity/{search_result.get('uri')}",
|
||||||
key=self.get_remote_id(search_result.get("uri")),
|
cover=self.get_cover_url(search_result.get("image")),
|
||||||
author=search_result.get("description"),
|
connector=self,
|
||||||
view_link=f"{self.base_url}/entity/{search_result.get('uri')}",
|
)
|
||||||
cover=self.get_cover_url(search_result.get("image")),
|
|
||||||
connector=self,
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_work_data(self, data):
|
def is_work_data(self, data):
|
||||||
return data.get("type") == "work"
|
return data.get("type") == "work"
|
||||||
|
|
|
@ -152,39 +152,41 @@ class Connector(AbstractConnector):
|
||||||
image_name = f"{cover_id}-{size}.jpg"
|
image_name = f"{cover_id}-{size}.jpg"
|
||||||
return f"{self.covers_url}/b/id/{image_name}"
|
return f"{self.covers_url}/b/id/{image_name}"
|
||||||
|
|
||||||
def parse_search_data(self, data):
|
def parse_search_data(self, data, min_confidence):
|
||||||
return data.get("docs")
|
for idx, search_result in enumerate(data.get("docs")):
|
||||||
|
# build the remote id from the openlibrary key
|
||||||
|
key = self.books_url + search_result["key"]
|
||||||
|
author = search_result.get("author_name") or ["Unknown"]
|
||||||
|
cover_blob = search_result.get("cover_i")
|
||||||
|
cover = self.get_cover_url([cover_blob], size="M") if cover_blob else None
|
||||||
|
|
||||||
def format_search_result(self, search_result):
|
# OL doesn't provide confidence, but it does sort by an internal ranking, so
|
||||||
# build the remote id from the openlibrary key
|
# this confidence value is relative to the list position
|
||||||
key = self.books_url + search_result["key"]
|
confidence = 1 / (idx + 1)
|
||||||
author = search_result.get("author_name") or ["Unknown"]
|
|
||||||
cover_blob = search_result.get("cover_i")
|
yield SearchResult(
|
||||||
cover = self.get_cover_url([cover_blob], size="M") if cover_blob else None
|
title=search_result.get("title"),
|
||||||
return SearchResult(
|
key=key,
|
||||||
title=search_result.get("title"),
|
author=", ".join(author),
|
||||||
key=key,
|
connector=self,
|
||||||
author=", ".join(author),
|
year=search_result.get("first_publish_year"),
|
||||||
connector=self,
|
cover=cover,
|
||||||
year=search_result.get("first_publish_year"),
|
confidence=confidence,
|
||||||
cover=cover,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
def parse_isbn_search_data(self, data):
|
def parse_isbn_search_data(self, data):
|
||||||
return list(data.values())
|
for search_result in list(data.values()):
|
||||||
|
# build the remote id from the openlibrary key
|
||||||
def format_isbn_search_result(self, search_result):
|
key = self.books_url + search_result["key"]
|
||||||
# build the remote id from the openlibrary key
|
authors = search_result.get("authors") or [{"name": "Unknown"}]
|
||||||
key = self.books_url + search_result["key"]
|
author_names = [author.get("name") for author in authors]
|
||||||
authors = search_result.get("authors") or [{"name": "Unknown"}]
|
yield SearchResult(
|
||||||
author_names = [author.get("name") for author in authors]
|
title=search_result.get("title"),
|
||||||
return SearchResult(
|
key=key,
|
||||||
title=search_result.get("title"),
|
author=", ".join(author_names),
|
||||||
key=key,
|
connector=self,
|
||||||
author=", ".join(author_names),
|
year=search_result.get("publish_date"),
|
||||||
connector=self,
|
)
|
||||||
year=search_result.get("publish_date"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def load_edition_data(self, olkey):
|
def load_edition_data(self, olkey):
|
||||||
"""query openlibrary for editions of a work"""
|
"""query openlibrary for editions of a work"""
|
||||||
|
|
|
@ -4,6 +4,7 @@ from django import forms
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.models.fields import ClearableFileInputWithWarning
|
from bookwyrm.models.fields import ClearableFileInputWithWarning
|
||||||
from .custom_form import CustomForm
|
from .custom_form import CustomForm
|
||||||
|
from .widgets import ArrayWidget, SelectDateWidget, Select
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=missing-class-docstring
|
# pylint: disable=missing-class-docstring
|
||||||
|
@ -14,14 +15,6 @@ class CoverForm(CustomForm):
|
||||||
help_texts = {f: None for f in fields}
|
help_texts = {f: None for f in fields}
|
||||||
|
|
||||||
|
|
||||||
class ArrayWidget(forms.widgets.TextInput):
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
# pylint: disable=no-self-use
|
|
||||||
def value_from_datadict(self, data, files, name):
|
|
||||||
"""get all values for this name"""
|
|
||||||
return [i for i in data.getlist(name) if i]
|
|
||||||
|
|
||||||
|
|
||||||
class EditionForm(CustomForm):
|
class EditionForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Edition
|
model = models.Edition
|
||||||
|
@ -56,16 +49,16 @@ class EditionForm(CustomForm):
|
||||||
"publishers": forms.TextInput(
|
"publishers": forms.TextInput(
|
||||||
attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
|
attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
|
||||||
),
|
),
|
||||||
"first_published_date": forms.SelectDateWidget(
|
"first_published_date": SelectDateWidget(
|
||||||
attrs={"aria-describedby": "desc_first_published_date"}
|
attrs={"aria-describedby": "desc_first_published_date"}
|
||||||
),
|
),
|
||||||
"published_date": forms.SelectDateWidget(
|
"published_date": SelectDateWidget(
|
||||||
attrs={"aria-describedby": "desc_published_date"}
|
attrs={"aria-describedby": "desc_published_date"}
|
||||||
),
|
),
|
||||||
"cover": ClearableFileInputWithWarning(
|
"cover": ClearableFileInputWithWarning(
|
||||||
attrs={"aria-describedby": "desc_cover"}
|
attrs={"aria-describedby": "desc_cover"}
|
||||||
),
|
),
|
||||||
"physical_format": forms.Select(
|
"physical_format": Select(
|
||||||
attrs={"aria-describedby": "desc_physical_format"}
|
attrs={"aria-describedby": "desc_physical_format"}
|
||||||
),
|
),
|
||||||
"physical_format_detail": forms.TextInput(
|
"physical_format_detail": forms.TextInput(
|
||||||
|
@ -85,3 +78,27 @@ class EditionForm(CustomForm):
|
||||||
),
|
),
|
||||||
"ASIN": forms.TextInput(attrs={"aria-describedby": "desc_ASIN"}),
|
"ASIN": forms.TextInput(attrs={"aria-describedby": "desc_ASIN"}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EditionFromWorkForm(CustomForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
# make all fields hidden
|
||||||
|
for visible in self.visible_fields():
|
||||||
|
visible.field.widget = forms.HiddenInput()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Work
|
||||||
|
fields = [
|
||||||
|
"title",
|
||||||
|
"subtitle",
|
||||||
|
"authors",
|
||||||
|
"description",
|
||||||
|
"languages",
|
||||||
|
"series",
|
||||||
|
"series_number",
|
||||||
|
"subjects",
|
||||||
|
"subject_places",
|
||||||
|
"cover",
|
||||||
|
"first_published_date",
|
||||||
|
]
|
||||||
|
|
|
@ -45,7 +45,7 @@ class ReportForm(CustomForm):
|
||||||
|
|
||||||
class ReadThroughForm(CustomForm):
|
class ReadThroughForm(CustomForm):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""make sure the email isn't in use by a registered user"""
|
"""don't let readthroughs end before they start"""
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
start_date = cleaned_data.get("start_date")
|
start_date = cleaned_data.get("start_date")
|
||||||
finish_date = cleaned_data.get("finish_date")
|
finish_date = cleaned_data.get("finish_date")
|
||||||
|
@ -53,7 +53,12 @@ class ReadThroughForm(CustomForm):
|
||||||
self.add_error(
|
self.add_error(
|
||||||
"finish_date", _("Reading finish date cannot be before start date.")
|
"finish_date", _("Reading finish date cannot be before start date.")
|
||||||
)
|
)
|
||||||
|
stopped_date = cleaned_data.get("stopped_date")
|
||||||
|
if start_date and stopped_date and start_date > stopped_date:
|
||||||
|
self.add_error(
|
||||||
|
"stopped_date", _("Reading stopped date cannot be before start date.")
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.ReadThrough
|
model = models.ReadThrough
|
||||||
fields = ["user", "book", "start_date", "finish_date"]
|
fields = ["user", "book", "start_date", "finish_date", "stopped_date"]
|
||||||
|
|
70
bookwyrm/forms/widgets.py
Normal file
70
bookwyrm/forms/widgets.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
""" using django model forms """
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
|
||||||
|
class ArrayWidget(forms.widgets.TextInput):
|
||||||
|
"""Inputs for postgres array fields"""
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
"""get all values for this name"""
|
||||||
|
return [i for i in data.getlist(name) if i]
|
||||||
|
|
||||||
|
|
||||||
|
class Select(forms.Select):
|
||||||
|
"""custom template for select widget"""
|
||||||
|
|
||||||
|
template_name = "widgets/select.html"
|
||||||
|
|
||||||
|
|
||||||
|
class SelectDateWidget(forms.SelectDateWidget):
|
||||||
|
"""
|
||||||
|
A widget that splits date input into two <select> boxes and a numerical year.
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_name = "widgets/addon_multiwidget.html"
|
||||||
|
select_widget = Select
|
||||||
|
|
||||||
|
def get_context(self, name, value, attrs):
|
||||||
|
"""sets individual widgets"""
|
||||||
|
context = super().get_context(name, value, attrs)
|
||||||
|
date_context = {}
|
||||||
|
year_name = self.year_field % name
|
||||||
|
date_context["year"] = forms.NumberInput().get_context(
|
||||||
|
name=year_name,
|
||||||
|
value=context["widget"]["value"]["year"],
|
||||||
|
attrs={
|
||||||
|
**context["widget"]["attrs"],
|
||||||
|
"id": f"id_{year_name}",
|
||||||
|
"class": "input",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
month_choices = list(self.months.items())
|
||||||
|
if not self.is_required:
|
||||||
|
month_choices.insert(0, self.month_none_value)
|
||||||
|
month_name = self.month_field % name
|
||||||
|
date_context["month"] = self.select_widget(
|
||||||
|
attrs, choices=month_choices
|
||||||
|
).get_context(
|
||||||
|
name=month_name,
|
||||||
|
value=context["widget"]["value"]["month"],
|
||||||
|
attrs={**context["widget"]["attrs"], "id": f"id_{month_name}"},
|
||||||
|
)
|
||||||
|
day_choices = [(i, i) for i in range(1, 32)]
|
||||||
|
if not self.is_required:
|
||||||
|
day_choices.insert(0, self.day_none_value)
|
||||||
|
day_name = self.day_field % name
|
||||||
|
date_context["day"] = self.select_widget(
|
||||||
|
attrs,
|
||||||
|
choices=day_choices,
|
||||||
|
).get_context(
|
||||||
|
name=day_name,
|
||||||
|
value=context["widget"]["value"]["day"],
|
||||||
|
attrs={**context["widget"]["attrs"], "id": f"id_{day_name}"},
|
||||||
|
)
|
||||||
|
subwidgets = []
|
||||||
|
for field in self._parse_date_fmt():
|
||||||
|
subwidgets.append(date_context[field]["widget"])
|
||||||
|
context["widget"]["subwidgets"] = subwidgets
|
||||||
|
return context
|
|
@ -1,6 +1,7 @@
|
||||||
""" import classes """
|
""" import classes """
|
||||||
|
|
||||||
from .importer import Importer
|
from .importer import Importer
|
||||||
|
from .calibre_import import CalibreImporter
|
||||||
from .goodreads_import import GoodreadsImporter
|
from .goodreads_import import GoodreadsImporter
|
||||||
from .librarything_import import LibrarythingImporter
|
from .librarything_import import LibrarythingImporter
|
||||||
from .openlibrary_import import OpenLibraryImporter
|
from .openlibrary_import import OpenLibraryImporter
|
||||||
|
|
28
bookwyrm/importers/calibre_import.py
Normal file
28
bookwyrm/importers/calibre_import.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
""" handle reading a csv from calibre """
|
||||||
|
from bookwyrm.models import Shelf
|
||||||
|
|
||||||
|
from . import Importer
|
||||||
|
|
||||||
|
|
||||||
|
class CalibreImporter(Importer):
|
||||||
|
"""csv downloads from Calibre"""
|
||||||
|
|
||||||
|
service = "Calibre"
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# Add timestamp to row_mappings_guesses for date_added to avoid
|
||||||
|
# integrity error
|
||||||
|
row_mappings_guesses = []
|
||||||
|
|
||||||
|
for field, mapping in self.row_mappings_guesses:
|
||||||
|
if field in ("date_added",):
|
||||||
|
row_mappings_guesses.append((field, mapping + ["timestamp"]))
|
||||||
|
else:
|
||||||
|
row_mappings_guesses.append((field, mapping))
|
||||||
|
|
||||||
|
self.row_mappings_guesses = row_mappings_guesses
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_shelf(self, normalized_row):
|
||||||
|
# Calibre export does not indicate which shelf to use. Go with a default one for now
|
||||||
|
return Shelf.TO_READ
|
|
@ -1,5 +1,8 @@
|
||||||
""" handle reading a tsv from librarything """
|
""" handle reading a tsv from librarything """
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from bookwyrm.models import Shelf
|
||||||
|
|
||||||
from . import Importer
|
from . import Importer
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,7 +24,7 @@ class LibrarythingImporter(Importer):
|
||||||
|
|
||||||
def get_shelf(self, normalized_row):
|
def get_shelf(self, normalized_row):
|
||||||
if normalized_row["date_finished"]:
|
if normalized_row["date_finished"]:
|
||||||
return "read"
|
return Shelf.READ_FINISHED
|
||||||
if normalized_row["date_started"]:
|
if normalized_row["date_started"]:
|
||||||
return "reading"
|
return Shelf.READING
|
||||||
return "to-read"
|
return Shelf.TO_READ
|
||||||
|
|
|
@ -56,12 +56,17 @@ class Command(BaseCommand):
|
||||||
self.stdout.write(" OK 🖼")
|
self.stdout.write(" OK 🖼")
|
||||||
|
|
||||||
# Books
|
# Books
|
||||||
books = models.Book.objects.select_subclasses().filter()
|
book_ids = (
|
||||||
self.stdout.write(
|
models.Book.objects.select_subclasses()
|
||||||
" → Book preview images ({}): ".format(len(books)), ending=""
|
.filter()
|
||||||
|
.values_list("id", flat=True)
|
||||||
)
|
)
|
||||||
for book in books:
|
|
||||||
preview_images.generate_edition_preview_image_task.delay(book.id)
|
self.stdout.write(
|
||||||
|
" → Book preview images ({}): ".format(len(book_ids)), ending=""
|
||||||
|
)
|
||||||
|
for book_id in book_ids:
|
||||||
|
preview_images.generate_edition_preview_image_task.delay(book_id)
|
||||||
self.stdout.write(".", ending="")
|
self.stdout.write(".", ending="")
|
||||||
self.stdout.write(" OK 🖼")
|
self.stdout.write(" OK 🖼")
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ def init_connectors():
|
||||||
covers_url="https://inventaire.io",
|
covers_url="https://inventaire.io",
|
||||||
search_url="https://inventaire.io/api/search?types=works&types=works&search=",
|
search_url="https://inventaire.io/api/search?types=works&types=works&search=",
|
||||||
isbn_search_url="https://inventaire.io/api/entities?action=by-uris&uris=isbn%3A",
|
isbn_search_url="https://inventaire.io/api/entities?action=by-uris&uris=isbn%3A",
|
||||||
priority=3,
|
priority=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
models.Connector.objects.create(
|
models.Connector.objects.create(
|
||||||
|
@ -101,20 +101,10 @@ def init_connectors():
|
||||||
covers_url="https://covers.openlibrary.org",
|
covers_url="https://covers.openlibrary.org",
|
||||||
search_url="https://openlibrary.org/search?q=",
|
search_url="https://openlibrary.org/search?q=",
|
||||||
isbn_search_url="https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:",
|
isbn_search_url="https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:",
|
||||||
priority=3,
|
priority=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def init_federated_servers():
|
|
||||||
"""big no to nazis"""
|
|
||||||
built_in_blocks = ["gab.ai", "gab.com"]
|
|
||||||
for server in built_in_blocks:
|
|
||||||
models.FederatedServer.objects.create(
|
|
||||||
server_name=server,
|
|
||||||
status="blocked",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def init_settings():
|
def init_settings():
|
||||||
"""info about the instance"""
|
"""info about the instance"""
|
||||||
models.SiteSettings.objects.create(
|
models.SiteSettings.objects.create(
|
||||||
|
@ -163,7 +153,6 @@ class Command(BaseCommand):
|
||||||
"group",
|
"group",
|
||||||
"permission",
|
"permission",
|
||||||
"connector",
|
"connector",
|
||||||
"federatedserver",
|
|
||||||
"settings",
|
"settings",
|
||||||
"linkdomain",
|
"linkdomain",
|
||||||
]
|
]
|
||||||
|
@ -176,8 +165,6 @@ class Command(BaseCommand):
|
||||||
init_permissions()
|
init_permissions()
|
||||||
if not limit or limit == "connector":
|
if not limit or limit == "connector":
|
||||||
init_connectors()
|
init_connectors()
|
||||||
if not limit or limit == "federatedserver":
|
|
||||||
init_federated_servers()
|
|
||||||
if not limit or limit == "settings":
|
if not limit or limit == "settings":
|
||||||
init_settings()
|
init_settings()
|
||||||
if not limit or limit == "linkdomain":
|
if not limit or limit == "linkdomain":
|
||||||
|
|
80
bookwyrm/migrations/0146_auto_20220316_2320.py
Normal file
80
bookwyrm/migrations/0146_auto_20220316_2320.py
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
# Generated by Django 3.2.12 on 2022-03-16 23:20
|
||||||
|
|
||||||
|
import bookwyrm.models.fields
|
||||||
|
from django.db import migrations
|
||||||
|
from bookwyrm.models import Shelf
|
||||||
|
|
||||||
|
|
||||||
|
def add_shelves(apps, schema_editor):
|
||||||
|
"""add any superusers to the "admin" group"""
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
shelf_model = apps.get_model("bookwyrm", "Shelf")
|
||||||
|
|
||||||
|
users = apps.get_model("bookwyrm", "User")
|
||||||
|
local_users = users.objects.using(db_alias).filter(local=True)
|
||||||
|
for user in local_users:
|
||||||
|
remote_id = f"{user.remote_id}/books/stopped"
|
||||||
|
shelf_model.objects.using(db_alias).create(
|
||||||
|
name="Stopped reading",
|
||||||
|
identifier=Shelf.STOPPED_READING,
|
||||||
|
user=user,
|
||||||
|
editable=False,
|
||||||
|
remote_id=remote_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0145_sitesettings_version"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="comment",
|
||||||
|
name="reading_status",
|
||||||
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("to-read", "To-Read"),
|
||||||
|
("reading", "Reading"),
|
||||||
|
("read", "Read"),
|
||||||
|
("stopped-reading", "Stopped-Reading"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="quotation",
|
||||||
|
name="reading_status",
|
||||||
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("to-read", "To-Read"),
|
||||||
|
("reading", "Reading"),
|
||||||
|
("read", "Read"),
|
||||||
|
("stopped-reading", "Stopped-Reading"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="review",
|
||||||
|
name="reading_status",
|
||||||
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("to-read", "To-Read"),
|
||||||
|
("reading", "Reading"),
|
||||||
|
("read", "Read"),
|
||||||
|
("stopped-reading", "Stopped-Reading"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(add_shelves, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
38
bookwyrm/migrations/0147_alter_user_preferred_language.py
Normal file
38
bookwyrm/migrations/0147_alter_user_preferred_language.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# Generated by Django 3.2.12 on 2022-03-26 16:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0146_auto_20220316_2352"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="preferred_language",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("en-us", "English"),
|
||||||
|
("de-de", "Deutsch (German)"),
|
||||||
|
("es-es", "Español (Spanish)"),
|
||||||
|
("gl-es", "Galego (Galician)"),
|
||||||
|
("it-it", "Italiano (Italian)"),
|
||||||
|
("fr-fr", "Français (French)"),
|
||||||
|
("lt-lt", "Lietuvių (Lithuanian)"),
|
||||||
|
("no-no", "Norsk (Norwegian)"),
|
||||||
|
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
||||||
|
("pt-pt", "Português Europeu (European Portuguese)"),
|
||||||
|
("ro-ro", "Română (Romanian)"),
|
||||||
|
("sv-se", "Svenska (Swedish)"),
|
||||||
|
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||||
|
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
39
bookwyrm/migrations/0148_alter_user_preferred_language.py
Normal file
39
bookwyrm/migrations/0148_alter_user_preferred_language.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# Generated by Django 3.2.12 on 2022-03-31 14:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0147_alter_user_preferred_language"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="preferred_language",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("en-us", "English"),
|
||||||
|
("de-de", "Deutsch (German)"),
|
||||||
|
("es-es", "Español (Spanish)"),
|
||||||
|
("gl-es", "Galego (Galician)"),
|
||||||
|
("it-it", "Italiano (Italian)"),
|
||||||
|
("fi-fi", "Suomi (Finnish)"),
|
||||||
|
("fr-fr", "Français (French)"),
|
||||||
|
("lt-lt", "Lietuvių (Lithuanian)"),
|
||||||
|
("no-no", "Norsk (Norwegian)"),
|
||||||
|
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
||||||
|
("pt-pt", "Português Europeu (European Portuguese)"),
|
||||||
|
("ro-ro", "Română (Romanian)"),
|
||||||
|
("sv-se", "Svenska (Swedish)"),
|
||||||
|
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||||
|
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
13
bookwyrm/migrations/0148_merge_20220326_2006.py
Normal file
13
bookwyrm/migrations/0148_merge_20220326_2006.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 3.2.12 on 2022-03-26 20:06
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0146_auto_20220316_2320"),
|
||||||
|
("bookwyrm", "0147_alter_user_preferred_language"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
13
bookwyrm/migrations/0149_merge_20220526_1716.py
Normal file
13
bookwyrm/migrations/0149_merge_20220526_1716.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 3.2.13 on 2022-05-26 17:16
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0148_alter_user_preferred_language"),
|
||||||
|
("bookwyrm", "0148_merge_20220326_2006"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
18
bookwyrm/migrations/0150_readthrough_stopped_date.py
Normal file
18
bookwyrm/migrations/0150_readthrough_stopped_date.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.13 on 2022-05-26 18:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0149_merge_20220526_1716"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="readthrough",
|
||||||
|
name="stopped_date",
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -8,6 +8,7 @@ from django.db.models import Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
from .fields import RemoteIdField
|
from .fields import RemoteIdField
|
||||||
|
@ -35,10 +36,11 @@ class BookWyrmModel(models.Model):
|
||||||
remote_id = RemoteIdField(null=True, activitypub_field="id")
|
remote_id = RemoteIdField(null=True, activitypub_field="id")
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""generate a url that resolves to the local object"""
|
"""generate the url that resolves to the local object, without a slug"""
|
||||||
base_path = f"https://{DOMAIN}"
|
base_path = f"https://{DOMAIN}"
|
||||||
if hasattr(self, "user"):
|
if hasattr(self, "user"):
|
||||||
base_path = f"{base_path}{self.user.local_path}"
|
base_path = f"{base_path}{self.user.local_path}"
|
||||||
|
|
||||||
model_name = type(self).__name__.lower()
|
model_name = type(self).__name__.lower()
|
||||||
return f"{base_path}/{model_name}/{self.id}"
|
return f"{base_path}/{model_name}/{self.id}"
|
||||||
|
|
||||||
|
@ -49,8 +51,20 @@ class BookWyrmModel(models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def local_path(self):
|
def local_path(self):
|
||||||
"""how to link to this object in the local app"""
|
"""how to link to this object in the local app, with a slug"""
|
||||||
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
|
local = self.get_remote_id().replace(f"https://{DOMAIN}", "")
|
||||||
|
|
||||||
|
name = None
|
||||||
|
if hasattr(self, "name_field"):
|
||||||
|
name = getattr(self, self.name_field)
|
||||||
|
elif hasattr(self, "name"):
|
||||||
|
name = self.name
|
||||||
|
|
||||||
|
if name:
|
||||||
|
slug = slugify(name)
|
||||||
|
local = f"{local}/s/{slug}"
|
||||||
|
|
||||||
|
return local
|
||||||
|
|
||||||
def raise_visible_to_user(self, viewer):
|
def raise_visible_to_user(self, viewer):
|
||||||
"""is a user authorized to view an object?"""
|
"""is a user authorized to view an object?"""
|
||||||
|
|
|
@ -176,8 +176,8 @@ class Book(BookDataModel):
|
||||||
"""properties of this edition, as a string"""
|
"""properties of this edition, as a string"""
|
||||||
items = [
|
items = [
|
||||||
self.physical_format if hasattr(self, "physical_format") else None,
|
self.physical_format if hasattr(self, "physical_format") else None,
|
||||||
self.languages[0] + " language"
|
f"{self.languages[0]} language"
|
||||||
if self.languages and self.languages[0] != "English"
|
if self.languages and self.languages[0] and self.languages[0] != "English"
|
||||||
else None,
|
else None,
|
||||||
str(self.published_date.year) if self.published_date else None,
|
str(self.published_date.year) if self.published_date else None,
|
||||||
", ".join(self.publishers) if hasattr(self, "publishers") else None,
|
", ".join(self.publishers) if hasattr(self, "publishers") else None,
|
||||||
|
|
|
@ -125,7 +125,7 @@ class ActivitypubFieldMixin:
|
||||||
"""model_field_name to activitypubFieldName"""
|
"""model_field_name to activitypubFieldName"""
|
||||||
if self.activitypub_field:
|
if self.activitypub_field:
|
||||||
return self.activitypub_field
|
return self.activitypub_field
|
||||||
name = self.name.split(".")[-1]
|
name = self.name.rsplit(".", maxsplit=1)[-1]
|
||||||
components = name.split("_")
|
components = name.split("_")
|
||||||
return components[0] + "".join(x.title() for x in components[1:])
|
return components[0] + "".join(x.title() for x in components[1:])
|
||||||
|
|
||||||
|
@ -389,7 +389,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
|
||||||
self.alt_field = alt_field
|
self.alt_field = alt_field
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# pylint: disable=arguments-differ
|
# pylint: disable=arguments-differ,arguments-renamed
|
||||||
def set_field_from_activity(self, instance, data, save=True, overwrite=True):
|
def set_field_from_activity(self, instance, data, save=True, overwrite=True):
|
||||||
"""helper function for assinging a value to the field"""
|
"""helper function for assinging a value to the field"""
|
||||||
value = getattr(data, self.get_activitypub_field())
|
value = getattr(data, self.get_activitypub_field())
|
||||||
|
|
|
@ -175,9 +175,15 @@ class ImportItem(models.Model):
|
||||||
def date_added(self):
|
def date_added(self):
|
||||||
"""when the book was added to this dataset"""
|
"""when the book was added to this dataset"""
|
||||||
if self.normalized_data.get("date_added"):
|
if self.normalized_data.get("date_added"):
|
||||||
return timezone.make_aware(
|
parsed_date_added = dateutil.parser.parse(
|
||||||
dateutil.parser.parse(self.normalized_data.get("date_added"))
|
self.normalized_data.get("date_added")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if timezone.is_aware(parsed_date_added):
|
||||||
|
# Keep timezone if import already had one
|
||||||
|
return parsed_date_added
|
||||||
|
|
||||||
|
return timezone.make_aware(parsed_date_added)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -27,6 +27,7 @@ class ReadThrough(BookWyrmModel):
|
||||||
)
|
)
|
||||||
start_date = models.DateTimeField(blank=True, null=True)
|
start_date = models.DateTimeField(blank=True, null=True)
|
||||||
finish_date = models.DateTimeField(blank=True, null=True)
|
finish_date = models.DateTimeField(blank=True, null=True)
|
||||||
|
stopped_date = models.DateTimeField(blank=True, null=True)
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
@ -34,7 +35,7 @@ class ReadThrough(BookWyrmModel):
|
||||||
cache.delete(f"latest_read_through-{self.user.id}-{self.book.id}")
|
cache.delete(f"latest_read_through-{self.user.id}-{self.book.id}")
|
||||||
self.user.update_active_date()
|
self.user.update_active_date()
|
||||||
# an active readthrough must have an unset finish date
|
# an active readthrough must have an unset finish date
|
||||||
if self.finish_date:
|
if self.finish_date or self.stopped_date:
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -39,15 +39,14 @@ class UserRelationship(BookWyrmModel):
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""clear the template cache"""
|
"""clear the template cache"""
|
||||||
# invalidate the template cache
|
clear_cache(self.user_subject, self.user_object)
|
||||||
cache.delete_many(
|
|
||||||
[
|
|
||||||
f"relationship-{self.user_subject.id}-{self.user_object.id}",
|
|
||||||
f"relationship-{self.user_object.id}-{self.user_subject.id}",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
"""clear the template cache"""
|
||||||
|
clear_cache(self.user_subject, self.user_object)
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""relationships should be unique"""
|
"""relationships should be unique"""
|
||||||
|
|
||||||
|
@ -90,7 +89,9 @@ class UserFollows(ActivityMixin, UserRelationship):
|
||||||
user_object=self.user_subject,
|
user_object=self.user_subject,
|
||||||
)
|
)
|
||||||
).exists():
|
).exists():
|
||||||
raise IntegrityError()
|
raise IntegrityError(
|
||||||
|
"Attempting to follow blocked user", self.user_subject, self.user_object
|
||||||
|
)
|
||||||
# don't broadcast this type of relationship -- accepts and requests
|
# don't broadcast this type of relationship -- accepts and requests
|
||||||
# are handled by the UserFollowRequest model
|
# are handled by the UserFollowRequest model
|
||||||
super().save(*args, broadcast=False, **kwargs)
|
super().save(*args, broadcast=False, **kwargs)
|
||||||
|
@ -98,11 +99,12 @@ class UserFollows(ActivityMixin, UserRelationship):
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_request(cls, follow_request):
|
def from_request(cls, follow_request):
|
||||||
"""converts a follow request into a follow relationship"""
|
"""converts a follow request into a follow relationship"""
|
||||||
return cls.objects.create(
|
obj, _ = cls.objects.get_or_create(
|
||||||
user_subject=follow_request.user_subject,
|
user_subject=follow_request.user_subject,
|
||||||
user_object=follow_request.user_object,
|
user_object=follow_request.user_object,
|
||||||
remote_id=follow_request.remote_id,
|
remote_id=follow_request.remote_id,
|
||||||
)
|
)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
|
@ -133,7 +135,9 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
user_object=self.user_subject,
|
user_object=self.user_subject,
|
||||||
)
|
)
|
||||||
).exists():
|
).exists():
|
||||||
raise IntegrityError()
|
raise IntegrityError(
|
||||||
|
"Attempting to follow blocked user", self.user_subject, self.user_object
|
||||||
|
)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
if broadcast and self.user_subject.local and not self.user_object.local:
|
if broadcast and self.user_subject.local and not self.user_object.local:
|
||||||
|
@ -174,7 +178,8 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
UserFollows.from_request(self)
|
UserFollows.from_request(self)
|
||||||
self.delete()
|
if self.id:
|
||||||
|
self.delete()
|
||||||
|
|
||||||
def reject(self):
|
def reject(self):
|
||||||
"""generate a Reject for this follow request"""
|
"""generate a Reject for this follow request"""
|
||||||
|
@ -207,3 +212,13 @@ class UserBlocks(ActivityMixin, UserRelationship):
|
||||||
Q(user_subject=self.user_subject, user_object=self.user_object)
|
Q(user_subject=self.user_subject, user_object=self.user_object)
|
||||||
| Q(user_subject=self.user_object, user_object=self.user_subject)
|
| Q(user_subject=self.user_object, user_object=self.user_subject)
|
||||||
).delete()
|
).delete()
|
||||||
|
|
||||||
|
|
||||||
|
def clear_cache(user_subject, user_object):
|
||||||
|
"""clear relationship cache"""
|
||||||
|
cache.delete_many(
|
||||||
|
[
|
||||||
|
f"relationship-{user_subject.id}-{user_object.id}",
|
||||||
|
f"relationship-{user_object.id}-{user_subject.id}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
|
from bookwyrm.settings import DOMAIN
|
||||||
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
|
@ -17,8 +18,9 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||||
TO_READ = "to-read"
|
TO_READ = "to-read"
|
||||||
READING = "reading"
|
READING = "reading"
|
||||||
READ_FINISHED = "read"
|
READ_FINISHED = "read"
|
||||||
|
STOPPED_READING = "stopped-reading"
|
||||||
|
|
||||||
READ_STATUS_IDENTIFIERS = (TO_READ, READING, READ_FINISHED)
|
READ_STATUS_IDENTIFIERS = (TO_READ, READING, READ_FINISHED, STOPPED_READING)
|
||||||
|
|
||||||
name = fields.CharField(max_length=100)
|
name = fields.CharField(max_length=100)
|
||||||
identifier = models.CharField(max_length=100)
|
identifier = models.CharField(max_length=100)
|
||||||
|
@ -65,6 +67,11 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||||
identifier = self.identifier or self.get_identifier()
|
identifier = self.identifier or self.get_identifier()
|
||||||
return f"{base_path}/books/{identifier}"
|
return f"{base_path}/books/{identifier}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def local_path(self):
|
||||||
|
"""No slugs"""
|
||||||
|
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
|
||||||
|
|
||||||
def raise_not_deletable(self, viewer):
|
def raise_not_deletable(self, viewer):
|
||||||
"""don't let anyone delete a default shelf"""
|
"""don't let anyone delete a default shelf"""
|
||||||
super().raise_not_deletable(viewer)
|
super().raise_not_deletable(viewer)
|
||||||
|
|
|
@ -116,11 +116,8 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
def ignore_activity(cls, activity): # pylint: disable=too-many-return-statements
|
def ignore_activity(cls, activity): # pylint: disable=too-many-return-statements
|
||||||
"""keep notes if they are replies to existing statuses"""
|
"""keep notes if they are replies to existing statuses"""
|
||||||
if activity.type == "Announce":
|
if activity.type == "Announce":
|
||||||
try:
|
boosted = activitypub.resolve_remote_id(activity.object, get_activity=True)
|
||||||
boosted = activitypub.resolve_remote_id(
|
if not boosted:
|
||||||
activity.object, get_activity=True
|
|
||||||
)
|
|
||||||
except activitypub.ActivitySerializerError:
|
|
||||||
# if we can't load the status, definitely ignore it
|
# if we can't load the status, definitely ignore it
|
||||||
return True
|
return True
|
||||||
# keep the boost if we would keep the status
|
# keep the boost if we would keep the status
|
||||||
|
@ -265,7 +262,7 @@ class GeneratedNote(Status):
|
||||||
|
|
||||||
|
|
||||||
ReadingStatusChoices = models.TextChoices(
|
ReadingStatusChoices = models.TextChoices(
|
||||||
"ReadingStatusChoices", ["to-read", "reading", "read"]
|
"ReadingStatusChoices", ["to-read", "reading", "read", "stopped-reading"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -306,10 +303,17 @@ class Comment(BookStatus):
|
||||||
@property
|
@property
|
||||||
def pure_content(self):
|
def pure_content(self):
|
||||||
"""indicate the book in question for mastodon (or w/e) users"""
|
"""indicate the book in question for mastodon (or w/e) users"""
|
||||||
return (
|
if self.progress_mode == "PG" and self.progress and (self.progress > 0):
|
||||||
f'{self.content}<p>(comment on <a href="{self.book.remote_id}">'
|
return_value = (
|
||||||
f'"{self.book.title}"</a>)</p>'
|
f'{self.content}<p>(comment on <a href="{self.book.remote_id}">'
|
||||||
)
|
f'"{self.book.title}"</a>, page {self.progress})</p>'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return_value = (
|
||||||
|
f'{self.content}<p>(comment on <a href="{self.book.remote_id}">'
|
||||||
|
f'"{self.book.title}"</a>)</p>'
|
||||||
|
)
|
||||||
|
return return_value
|
||||||
|
|
||||||
activity_serializer = activitypub.Comment
|
activity_serializer = activitypub.Comment
|
||||||
|
|
||||||
|
@ -335,10 +339,17 @@ class Quotation(BookStatus):
|
||||||
"""indicate the book in question for mastodon (or w/e) users"""
|
"""indicate the book in question for mastodon (or w/e) users"""
|
||||||
quote = re.sub(r"^<p>", '<p>"', self.quote)
|
quote = re.sub(r"^<p>", '<p>"', self.quote)
|
||||||
quote = re.sub(r"</p>$", '"</p>', quote)
|
quote = re.sub(r"</p>$", '"</p>', quote)
|
||||||
return (
|
if self.position_mode == "PG" and self.position and (self.position > 0):
|
||||||
f'{quote} <p>-- <a href="{self.book.remote_id}">'
|
return_value = (
|
||||||
f'"{self.book.title}"</a></p>{self.content}'
|
f'{quote} <p>-- <a href="{self.book.remote_id}">'
|
||||||
)
|
f'"{self.book.title}"</a>, page {self.position}</p>{self.content}'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return_value = (
|
||||||
|
f'{quote} <p>-- <a href="{self.book.remote_id}">'
|
||||||
|
f'"{self.book.title}"</a></p>{self.content}'
|
||||||
|
)
|
||||||
|
return return_value
|
||||||
|
|
||||||
activity_serializer = activitypub.Quotation
|
activity_serializer = activitypub.Quotation
|
||||||
|
|
||||||
|
@ -377,7 +388,7 @@ class Review(BookStatus):
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""clear rating caches"""
|
"""clear rating caches"""
|
||||||
if self.book.parent_work:
|
if self.book.parent_work:
|
||||||
cache.delete(f"book-rating-{self.book.parent_work.id}-*")
|
cache.delete(f"book-rating-{self.book.parent_work.id}")
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -374,6 +374,10 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
"name": "Read",
|
"name": "Read",
|
||||||
"identifier": "read",
|
"identifier": "read",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Stopped Reading",
|
||||||
|
"identifier": "stopped-reading",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for shelf in shelves:
|
for shelf in shelves:
|
||||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
env = Env()
|
env = Env()
|
||||||
env.read_env()
|
env.read_env()
|
||||||
DOMAIN = env("DOMAIN")
|
DOMAIN = env("DOMAIN")
|
||||||
VERSION = "0.3.4"
|
VERSION = "0.4.0"
|
||||||
|
|
||||||
RELEASE_API = env(
|
RELEASE_API = env(
|
||||||
"RELEASE_API",
|
"RELEASE_API",
|
||||||
|
@ -21,7 +21,7 @@ RELEASE_API = env(
|
||||||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||||
|
|
||||||
JS_CACHE = "bc93172a"
|
JS_CACHE = "e678183b"
|
||||||
|
|
||||||
# email
|
# email
|
||||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||||
|
@ -212,7 +212,7 @@ STREAMS = [
|
||||||
|
|
||||||
# Search configuration
|
# Search configuration
|
||||||
# total time in seconds that the instance will spend searching connectors
|
# total time in seconds that the instance will spend searching connectors
|
||||||
SEARCH_TIMEOUT = int(env("SEARCH_TIMEOUT", 15))
|
SEARCH_TIMEOUT = int(env("SEARCH_TIMEOUT", 8))
|
||||||
# timeout for a query to an individual connector
|
# timeout for a query to an individual connector
|
||||||
QUERY_TIMEOUT = int(env("QUERY_TIMEOUT", 5))
|
QUERY_TIMEOUT = int(env("QUERY_TIMEOUT", 5))
|
||||||
|
|
||||||
|
@ -284,11 +284,13 @@ LANGUAGES = [
|
||||||
("es-es", _("Español (Spanish)")),
|
("es-es", _("Español (Spanish)")),
|
||||||
("gl-es", _("Galego (Galician)")),
|
("gl-es", _("Galego (Galician)")),
|
||||||
("it-it", _("Italiano (Italian)")),
|
("it-it", _("Italiano (Italian)")),
|
||||||
|
("fi-fi", _("Suomi (Finnish)")),
|
||||||
("fr-fr", _("Français (French)")),
|
("fr-fr", _("Français (French)")),
|
||||||
("lt-lt", _("Lietuvių (Lithuanian)")),
|
("lt-lt", _("Lietuvių (Lithuanian)")),
|
||||||
("no-no", _("Norsk (Norwegian)")),
|
("no-no", _("Norsk (Norwegian)")),
|
||||||
("pt-br", _("Português do Brasil (Brazilian Portuguese)")),
|
("pt-br", _("Português do Brasil (Brazilian Portuguese)")),
|
||||||
("pt-pt", _("Português Europeu (European Portuguese)")),
|
("pt-pt", _("Português Europeu (European Portuguese)")),
|
||||||
|
("ro-ro", _("Română (Romanian)")),
|
||||||
("sv-se", _("Svenska (Swedish)")),
|
("sv-se", _("Svenska (Swedish)")),
|
||||||
("zh-hans", _("简体中文 (Simplified Chinese)")),
|
("zh-hans", _("简体中文 (Simplified Chinese)")),
|
||||||
("zh-hant", _("繁體中文 (Traditional Chinese)")),
|
("zh-hant", _("繁體中文 (Traditional Chinese)")),
|
||||||
|
|
|
@ -34,6 +34,18 @@ body {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: $scrollbar-thumb;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: $scrollbar-track;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -125,13 +137,6 @@ button:focus-visible .button-invisible-overlay {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tooltips
|
|
||||||
******************************************************************************/
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** States
|
/** States
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
||||||
|
|
|
@ -114,3 +114,17 @@ details[open] summary .details-close {
|
||||||
padding-bottom: 0.25rem;
|
padding-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Navbar details
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#navbar-dropdown .navbar-item {
|
||||||
|
color: $text;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.375rem 3rem 0.375rem 1rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navbar-dropdown .navbar-item:hover {
|
||||||
|
background-color: $background-secondary;
|
||||||
|
}
|
||||||
|
|
|
@ -23,3 +23,8 @@
|
||||||
.has-background-tertiary {
|
.has-background-tertiary {
|
||||||
background-color: $background-tertiary !important;
|
background-color: $background-tertiary !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Workaround for dark theme as .has-text-black doesn't give desired effect. */
|
||||||
|
.has-text-default {
|
||||||
|
color: $text !important;
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,9 @@ $primary: #005e50;
|
||||||
$primary-light: #1d2b28;
|
$primary-light: #1d2b28;
|
||||||
$info: #1f4666;
|
$info: #1f4666;
|
||||||
$success: #246447;
|
$success: #246447;
|
||||||
|
$success-light: #0d2f1e;
|
||||||
$warning: #8b6c15;
|
$warning: #8b6c15;
|
||||||
|
$warning-light: #372e13;
|
||||||
$danger: #872538;
|
$danger: #872538;
|
||||||
$danger-light: #481922;
|
$danger-light: #481922;
|
||||||
$light: #393939;
|
$light: #393939;
|
||||||
|
@ -26,6 +28,8 @@ $background-body: rgb(24, 27, 28);
|
||||||
$background-secondary: rgb(28, 30, 32);
|
$background-secondary: rgb(28, 30, 32);
|
||||||
$background-tertiary: rgb(32, 34, 36);
|
$background-tertiary: rgb(32, 34, 36);
|
||||||
$modal-background-background-color: rgba($black, 0.8);
|
$modal-background-background-color: rgba($black, 0.8);
|
||||||
|
$scrollbar-track: $background-secondary;
|
||||||
|
$scrollbar-thumb: $light;
|
||||||
|
|
||||||
/* highlight colors */
|
/* highlight colors */
|
||||||
$primary-highlight: $primary;
|
$primary-highlight: $primary;
|
||||||
|
@ -49,6 +53,7 @@ $link-hover: $white-bis;
|
||||||
$link-hover-border: #51595d;
|
$link-hover-border: #51595d;
|
||||||
$link-focus: $white-bis;
|
$link-focus: $white-bis;
|
||||||
$link-active: $white-bis;
|
$link-active: $white-bis;
|
||||||
|
$link-light: #0d1c26;
|
||||||
|
|
||||||
/* bulma overrides */
|
/* bulma overrides */
|
||||||
$background: $background-secondary;
|
$background: $background-secondary;
|
||||||
|
@ -79,5 +84,13 @@ $progress-value-background-color: $border-light;
|
||||||
$family-primary: $family-sans-serif;
|
$family-primary: $family-sans-serif;
|
||||||
$family-secondary: $family-sans-serif;
|
$family-secondary: $family-sans-serif;
|
||||||
|
|
||||||
|
.has-text-muted {
|
||||||
|
color: $grey-lighter !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-text-more-muted {
|
||||||
|
color: $grey-light !important;
|
||||||
|
}
|
||||||
|
|
||||||
@import "../bookwyrm";
|
@import "../bookwyrm";
|
||||||
@import "../vendor/icons.css";
|
@import "../vendor/icons.css";
|
||||||
|
|
|
@ -19,6 +19,8 @@ $scheme-main: $white-bis;
|
||||||
$background-body: $white;
|
$background-body: $white;
|
||||||
$background-secondary: $white-ter;
|
$background-secondary: $white-ter;
|
||||||
$background-tertiary: $white-bis;
|
$background-tertiary: $white-bis;
|
||||||
|
$scrollbar-track: $background-secondary;
|
||||||
|
$scrollbar-thumb: $grey-lighter;
|
||||||
|
|
||||||
/* highlight colors */
|
/* highlight colors */
|
||||||
$primary-highlight: $primary-light;
|
$primary-highlight: $primary-light;
|
||||||
|
@ -55,5 +57,14 @@ $invisible-overlay-background-color: rgba($scheme-invert, 0.66);
|
||||||
$family-primary: $family-sans-serif;
|
$family-primary: $family-sans-serif;
|
||||||
$family-secondary: $family-sans-serif;
|
$family-secondary: $family-sans-serif;
|
||||||
|
|
||||||
|
|
||||||
|
.has-text-muted {
|
||||||
|
color: $grey-dark !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-text-more-muted {
|
||||||
|
color: $grey !important;
|
||||||
|
}
|
||||||
|
|
||||||
@import "../bookwyrm";
|
@import "../bookwyrm";
|
||||||
@import "../vendor/icons.css";
|
@import "../vendor/icons.css";
|
||||||
|
|
|
@ -203,6 +203,8 @@ let StatusCache = new (class {
|
||||||
.forEach((item) => (item.disabled = false));
|
.forEach((item) => (item.disabled = false));
|
||||||
|
|
||||||
next_identifier = next_identifier == "complete" ? "read" : next_identifier;
|
next_identifier = next_identifier == "complete" ? "read" : next_identifier;
|
||||||
|
next_identifier =
|
||||||
|
next_identifier == "stopped-reading-complete" ? "stopped-reading" : next_identifier;
|
||||||
|
|
||||||
// Disable the current state
|
// Disable the current state
|
||||||
button.querySelector(
|
button.querySelector(
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
<p>
|
<p>
|
||||||
{% url "conduct" as coc_path %}
|
{% url "conduct" as coc_path %}
|
||||||
{% blocktrans trimmed with site_name=site.name %}
|
{% blocktrans trimmed with site_name=site.name %}
|
||||||
{{ site_name }}'s moderators and administrators keep the site up and running, enforce the <a href="coc_path">code of conduct</a>, and respond when users report spam and bad behavior.
|
{{ site_name }}'s moderators and administrators keep the site up and running, enforce the <a href="{{ coc_path }}">code of conduct</a>, and respond when users report spam and bad behavior.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="column">
|
<div class="column is-clipped">
|
||||||
{% block about_content %}{% endblock %}
|
{% block about_content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form class="block" name="edit-author" action="{{ author.local_path }}/edit" method="post">
|
<form class="block" name="edit-author" action="{% url 'edit-author' author.id %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
||||||
|
|
||||||
|
|
|
@ -208,9 +208,17 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% if book.parent_work.editions.count > 1 %}
|
{% with work=book.parent_work %}
|
||||||
<p>{% blocktrans with path=book.parent_work.local_path count=book.parent_work.editions.count %}<a href="{{ path }}/editions">{{ count }} editions</a>{% endblocktrans %}</p>
|
<p>
|
||||||
{% endif %}
|
<a href="{{ work.local_path }}/editions">
|
||||||
|
{% blocktrans trimmed count counter=work.editions.count with count=work.editions.count|intcomma %}
|
||||||
|
{{ count }} edition
|
||||||
|
{% plural %}
|
||||||
|
{{ count }} editions
|
||||||
|
{% endblocktrans %}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# user's relationship to the book #}
|
{# user's relationship to the book #}
|
||||||
|
@ -276,7 +284,7 @@
|
||||||
{% if user_statuses.review_count or user_statuses.comment_count or user_statuses.quotation_count %}
|
{% if user_statuses.review_count or user_statuses.comment_count or user_statuses.quotation_count %}
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<ul>
|
<ul>
|
||||||
{% url 'book' book.id as tab_url %}
|
{% url 'book' book.id book.name|slugify as tab_url %}
|
||||||
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||||
<a href="{{ tab_url }}#reviews">{% trans "Reviews" %} ({{ review_count }})</a>
|
<a href="{{ tab_url }}#reviews">{% trans "Reviews" %} ({{ review_count }})</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -3,18 +3,24 @@
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
{% load utilities %}
|
{% load utilities %}
|
||||||
|
|
||||||
{% block title %}{% if book %}{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}{% else %}{% trans "Add Book" %}{% endif %}{% endblock %}
|
{% block title %}
|
||||||
|
{% if book.title %}
|
||||||
|
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Add Book" %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<header class="block">
|
<header class="block">
|
||||||
<h1 class="title level-left">
|
<h1 class="title level-left">
|
||||||
{% if book %}
|
{% if book.title %}
|
||||||
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
|
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans "Add Book" %}
|
{% trans "Add Book" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
{% if book %}
|
{% if book.created_date %}
|
||||||
<dl>
|
<dl>
|
||||||
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Added:" %}</dt>
|
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Added:" %}</dt>
|
||||||
<dd class="ml-2">{{ book.created_date | naturaltime }}</dd>
|
<dd class="ml-2">{{ book.created_date | naturaltime }}</dd>
|
||||||
|
@ -33,12 +39,20 @@
|
||||||
|
|
||||||
<form
|
<form
|
||||||
class="block"
|
class="block"
|
||||||
{% if book %}
|
{% if book.id %}
|
||||||
name="edit-book"
|
name="edit-book"
|
||||||
action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
|
{% if confirm_mode %}
|
||||||
|
action="{% url 'edit-book-confirm' book.id %}"
|
||||||
|
{% else %}
|
||||||
|
action="{% url 'edit-book' book.id %}"
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
name="create-book"
|
name="create-book"
|
||||||
action="/create-book{% if confirm_mode %}/confirm{% endif %}"
|
{% if confirm_mode %}
|
||||||
|
action="{% url 'create-book-confirm' %}"
|
||||||
|
{% else %}
|
||||||
|
action="{% url 'create-book' %}"
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
method="post"
|
method="post"
|
||||||
enctype="multipart/form-data"
|
enctype="multipart/form-data"
|
||||||
|
@ -97,7 +111,7 @@
|
||||||
<input type="radio" name="parent_work" value="{{ match.parent_work.id }}"> {{ match.parent_work.title }}
|
<input type="radio" name="parent_work" value="{{ match.parent_work.id }}"> {{ match.parent_work.title }}
|
||||||
</label>
|
</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<label>
|
<label class="label mt-2">
|
||||||
<input type="radio" name="parent_work" value="0" required> {% trans "This is a new work" %}
|
<input type="radio" name="parent_work" value="0" required> {% trans "This is a new work" %}
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
@ -119,7 +133,7 @@
|
||||||
{% if not confirm_mode %}
|
{% if not confirm_mode %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||||
{% if book %}
|
{% if book.id %}
|
||||||
<a class="button" href="{{ book.local_path }}">{% trans "Cancel" %}</a>
|
<a class="button" href="{{ book.local_path }}">{% trans "Cancel" %}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/" class="button" data-back>
|
<a href="/" class="button" data-back>
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
||||||
|
<input type="hidden" name="parent_work" value="{% firstof book.parent_work.id form.parent_work %}">
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<section class="block">
|
<section class="block">
|
||||||
|
@ -153,8 +155,7 @@
|
||||||
<label class="label" for="id_first_published_date">
|
<label class="label" for="id_first_published_date">
|
||||||
{% trans "First published date:" %}
|
{% trans "First published date:" %}
|
||||||
</label>
|
</label>
|
||||||
<input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if form.first_published_date.value %} value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %} aria-describedby="desc_first_published_date">
|
{{ form.first_published_date }}
|
||||||
|
|
||||||
{% include 'snippets/form_errors.html' with errors_list=form.first_published_date.errors id="desc_first_published_date" %}
|
{% include 'snippets/form_errors.html' with errors_list=form.first_published_date.errors id="desc_first_published_date" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -162,7 +163,7 @@
|
||||||
<label class="label" for="id_published_date">
|
<label class="label" for="id_published_date">
|
||||||
{% trans "Published date:" %}
|
{% trans "Published date:" %}
|
||||||
</label>
|
</label>
|
||||||
<input type="date" name="published_date" class="input" id="id_published_date"{% if form.published_date.value %} value="{{ form.published_date.value|date:'Y-m-d'}}"{% endif %} aria-describedby="desc_published_date">
|
{{ form.published_date }}
|
||||||
|
|
||||||
{% include 'snippets/form_errors.html' with errors_list=form.published_date.errors id="desc_published_date" %}
|
{% include 'snippets/form_errors.html' with errors_list=form.published_date.errors id="desc_published_date" %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -175,6 +176,8 @@
|
||||||
</h2>
|
</h2>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
{% if book.authors.exists %}
|
{% if book.authors.exists %}
|
||||||
|
{# preserve authors if the book is unsaved #}
|
||||||
|
<input type="hidden" name="authors" value="{% for author in book.authors.all %}{{ author.id }},{% endfor %}">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{% for author in book.authors.all %}
|
{% for author in book.authors.all %}
|
||||||
<div class="is-flex is-justify-content-space-between">
|
<div class="is-flex is-justify-content-space-between">
|
||||||
|
@ -255,9 +258,7 @@
|
||||||
<label class="label" for="id_physical_format">
|
<label class="label" for="id_physical_format">
|
||||||
{% trans "Format:" %}
|
{% trans "Format:" %}
|
||||||
</label>
|
</label>
|
||||||
<div class="select">
|
{{ form.physical_format }}
|
||||||
{{ form.physical_format }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% include 'snippets/form_errors.html' with errors_list=form.physical_format.errors id="desc_physical_format" %}
|
{% include 'snippets/form_errors.html' with errors_list=form.physical_format.errors id="desc_physical_format" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
<div class="column my-3-mobile ml-3-tablet mr-auto">
|
<div class="column my-3-mobile ml-3-tablet mr-auto">
|
||||||
<h2 class="title is-5 mb-1">
|
<h2 class="title is-5 mb-1">
|
||||||
<a href="{{ book.local_path }}" class="has-text-black">
|
<a href="{{ book.local_path }}" class="has-text-default">
|
||||||
{{ book|book_title }}
|
{{ book|book_title }}
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -46,7 +46,36 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="block">
|
||||||
{% include 'snippets/pagination.html' with page=editions path=request.path %}
|
{% include 'snippets/pagination.html' with page=editions path=request.path %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="block has-text-centered help">
|
||||||
|
<p>
|
||||||
|
{% trans "Can't find the edition you're looking for?" %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form action="{% url 'create-book-data' %}" method="POST" name="add-edition-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ work_form.title }}
|
||||||
|
{{ work_form.subtitle }}
|
||||||
|
{{ work_form.authors }}
|
||||||
|
{{ work_form.description }}
|
||||||
|
{{ work_form.languages }}
|
||||||
|
{{ work_form.series }}
|
||||||
|
{{ work_form.cover }}
|
||||||
|
{{ work_form.first_published_date }}
|
||||||
|
{% for subject in work.subjects %}
|
||||||
|
<input type="hidden" name="subjects" value="{{ subject }}">
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<input type="hidden" name="parent_work" value="{{ work.id }}">
|
||||||
|
<div>
|
||||||
|
<button class="button is-small" type="submit">
|
||||||
|
{% trans "Add another edition" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{% url 'book' book.id %}">{{ book|book_title }}</a></li>
|
<li><a href="{% url 'book' book.id book.name|slugify %}">{{ book|book_title }}</a></li>
|
||||||
<li class="is-active">
|
<li class="is-active">
|
||||||
<a href="#" aria-current="page">
|
<a href="#" aria-current="page">
|
||||||
{% trans "Edit links" %}
|
{% trans "Edit links" %}
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% trans "Help" as button_text %}
|
|
||||||
{% include 'snippets/toggle/open_button.html' with text=button_text class="ml-3 is-rounded is-small has-background-body p-0 pb-1" icon="question-circle is-size-6" controls_text=controls_text controls_uid=controls_uid %}
|
|
||||||
|
|
||||||
<aside class="tooltip notification is-hidden transition-y is-pulled-left mb-2" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}">
|
|
||||||
{% trans "Close" as button_text %}
|
|
||||||
{% include 'snippets/toggle/close_button.html' with label=button_text class="delete" nonbutton=True controls_text=controls_text controls_uid=controls_uid %}
|
|
||||||
|
|
||||||
{% block tooltip_content %}{% endblock %}
|
|
||||||
</aside>
|
|
|
@ -29,9 +29,16 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="block">
|
<section class="block">
|
||||||
{% trans "Can't find your code?" as button_text %}
|
<form name="fallback" method="GET" action="{% url 'resend-link' %}" autocomplete="off">
|
||||||
{% include "snippets/toggle/open_button.html" with text=button_text controls_text="resend_form" focus="resend_form_header" %}
|
<button
|
||||||
{% include "confirm_email/resend_form.html" with controls_text="resend_form" %}
|
type="submit"
|
||||||
|
class="button"
|
||||||
|
data-modal-open="resend_form"
|
||||||
|
>
|
||||||
|
{% trans "Can't find your code?" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% include "confirm_email/resend_modal.html" with id="resend_form" %}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
10
bookwyrm/templates/confirm_email/resend.html
Normal file
10
bookwyrm/templates/confirm_email/resend.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends 'landing/layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% trans "Resend confirmation link" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include "confirm_email/resend_modal.html" with active=True static=True id="resend-modal" %}
|
||||||
|
{% endblock %}
|
|
@ -1,20 +0,0 @@
|
||||||
{% extends "components/inline_form.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% block header %}
|
|
||||||
{% trans "Resend confirmation link" %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block form %}
|
|
||||||
<form name="resend" method="post" action="{% url 'resend-link' %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" for="email">{% trans "Email address:" %}</label>
|
|
||||||
<div class="control">
|
|
||||||
<input type="text" name="email" class="input" required id="email">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-link">{% trans "Resend link" %}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
44
bookwyrm/templates/confirm_email/resend_modal.html
Normal file
44
bookwyrm/templates/confirm_email/resend_modal.html
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{% extends "components/modal.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block modal-title %}
|
||||||
|
{% trans "Resend confirmation link" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-form-open %}
|
||||||
|
<form name="resend" method="post" action="{% url 'resend-link' %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-body %}
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="email">{% trans "Email address:" %}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
class="input"
|
||||||
|
id="email"
|
||||||
|
aria-described-by="id_email_errors"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
{% if error %}
|
||||||
|
<div id="id_email_errors">
|
||||||
|
<p class="help is-danger">
|
||||||
|
{% trans "No user matching this email address found." %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-footer %}
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-link">{% trans "Resend link" %}</button>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-form-close %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -43,7 +43,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
<a href="https://joinbookwyrm.com/">
|
<a href="https://joinbookwyrm.com/">
|
||||||
{% trans "Join Bookwyrm" %}
|
{% trans "Join BookWyrm" %}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -5,7 +5,19 @@
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2 class="title is-4">{% trans "Your Books" %}</h2>
|
<h2 class="title is-4">{% trans "Your Books" %}</h2>
|
||||||
{% if not suggested_books %}
|
{% if not suggested_books %}
|
||||||
<p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p>
|
|
||||||
|
<div class="content">
|
||||||
|
<p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p>
|
||||||
|
|
||||||
|
<div class="box has-background-link-light">
|
||||||
|
<p>{% trans "Do you have book data from another service like GoodReads?" %}</p>
|
||||||
|
<a href="{% url 'import' %}">
|
||||||
|
<span class="icon icon-list" aria-hidden="true"></span>
|
||||||
|
{% trans "Import your reading history" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% with active_book=request.GET.book %}
|
{% with active_book=request.GET.book %}
|
||||||
<div class="tab-group">
|
<div class="tab-group">
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
|
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
|
||||||
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
|
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
|
||||||
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
|
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
|
||||||
|
{% elif shelf.identifier == 'stopped-reading' %}{% trans "Stopped Reading" %}
|
||||||
{% else %}{{ shelf.name }}{% endif %}
|
{% else %}{{ shelf.name }}{% endif %}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
{% for membership in group.memberships.all %}
|
{% for membership in group.memberships.all %}
|
||||||
{% with member=membership.user %}
|
{% with member=membership.user %}
|
||||||
<div class="box has-text-centered is-shadowless has-background-tertiary my-2 mx-2 member_{{ member.id }}">
|
<div class="box has-text-centered is-shadowless has-background-tertiary my-2 mx-2 member_{{ member.id }}">
|
||||||
<a href="{{ member.local_path }}" class="has-text-black">
|
<a href="{{ member.local_path }}" class="has-text-default">
|
||||||
{% include 'snippets/avatar.html' with user=member large=True %}
|
{% include 'snippets/avatar.html' with user=member large=True %}
|
||||||
<span title="{{ member.display_name }}" class="is-block is-6 has-text-weight-bold">{{ member.display_name|truncatechars:10 }}</span>
|
<span title="{{ member.display_name }}" class="is-block is-6 has-text-weight-bold">{{ member.display_name|truncatechars:10 }}</span>
|
||||||
<span title="@{{ member|username }}" class="is-block pb-3">@{{ member|username|truncatechars:8 }}</span>
|
<span title="@{{ member|username }}" class="is-block pb-3">@{{ member|username|truncatechars:8 }}</span>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<div class="column is-flex is-flex-grow-0">
|
<div class="column is-flex is-flex-grow-0">
|
||||||
{% for user in suggested_users %}
|
{% for user in suggested_users %}
|
||||||
<div class="box has-text-centered is-shadowless has-background-tertiary m-2">
|
<div class="box has-text-centered is-shadowless has-background-tertiary m-2">
|
||||||
<a href="{{ user.local_path }}" class="has-text-black">
|
<a href="{{ user.local_path }}" class="has-text-default">
|
||||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||||
<span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span>
|
<span title="{{ user.display_name }}" class="is-block is-6 has-text-weight-bold">{{ user.display_name|truncatechars:10 }}</span>
|
||||||
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
|
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
|
||||||
|
|
|
@ -14,28 +14,35 @@
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-pulled-left" for="source">
|
<label class="label" for="source">
|
||||||
{% trans "Data source:" %}
|
{% trans "Data source:" %}
|
||||||
</label>
|
</label>
|
||||||
{% include 'import/tooltip.html' with controls_text="goodreads-tooltip" %}
|
|
||||||
|
<div class="select">
|
||||||
|
<select name="source" id="source" aria-describedby="desc_source">
|
||||||
|
<option value="Goodreads" {% if current == 'Goodreads' %}selected{% endif %}>
|
||||||
|
Goodreads (CSV)
|
||||||
|
</option>
|
||||||
|
<option value="Storygraph" {% if current == 'Storygraph' %}selected{% endif %}>
|
||||||
|
Storygraph (CSV)
|
||||||
|
</option>
|
||||||
|
<option value="LibraryThing" {% if current == 'LibraryThing' %}selected{% endif %}>
|
||||||
|
LibraryThing (TSV)
|
||||||
|
</option>
|
||||||
|
<option value="OpenLibrary" {% if current == 'OpenLibrary' %}selected{% endif %}>
|
||||||
|
OpenLibrary (CSV)
|
||||||
|
</option>
|
||||||
|
<option value="Calibre" {% if current == 'Calibre' %}selected{% endif %}>
|
||||||
|
Calibre (CSV)
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="help" id="desc_source">
|
||||||
|
{% trans 'You can download your Goodreads data from the <a href="https://www.goodreads.com/review/import" target="_blank" rel="noopener noreferrer">Import/Export page</a> of your Goodreads account.' %}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="select block">
|
|
||||||
<select name="source" id="source">
|
|
||||||
<option value="Goodreads" {% if current == 'Goodreads' %}selected{% endif %}>
|
|
||||||
Goodreads (CSV)
|
|
||||||
</option>
|
|
||||||
<option value="Storygraph" {% if current == 'Storygraph' %}selected{% endif %}>
|
|
||||||
Storygraph (CSV)
|
|
||||||
</option>
|
|
||||||
<option value="LibraryThing" {% if current == 'LibraryThing' %}selected{% endif %}>
|
|
||||||
LibraryThing (TSV)
|
|
||||||
</option>
|
|
||||||
<option value="OpenLibrary" {% if current == 'OpenLibrary' %}selected{% endif %}>
|
|
||||||
OpenLibrary (CSV)
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_csv_file">{% trans "Data file:" %}</label>
|
<label class="label" for="id_csv_file">{% trans "Data file:" %}</label>
|
||||||
{{ import_form.csv_file }}
|
{{ import_form.csv_file }}
|
||||||
|
@ -63,7 +70,7 @@
|
||||||
<div class="content block">
|
<div class="content block">
|
||||||
<h2 class="title">{% trans "Recent Imports" %}</h2>
|
<h2 class="title">{% trans "Recent Imports" %}</h2>
|
||||||
{% if not jobs %}
|
{% if not jobs %}
|
||||||
<p>{% trans "No recent imports" %}</p>
|
<p><em>{% trans "No recent imports" %}</em></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for job in jobs %}
|
{% for job in jobs %}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
{% extends 'components/tooltip.html' %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block tooltip_content %}
|
|
||||||
|
|
||||||
{% trans 'You can download your Goodreads data from the <a href="https://www.goodreads.com/review/import" target="_blank" rel="noopener noreferrer">Import/Export page</a> of your Goodreads account.' %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -90,64 +90,8 @@
|
||||||
|
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<div class="navbar-item mt-3 py-0 has-dropdown is-hoverable">
|
<div class="navbar-item mt-3 py-0">
|
||||||
<a
|
{% include 'user_menu.html' %}
|
||||||
href="{{ request.user.local_path }}"
|
|
||||||
class="navbar-link pulldown-menu"
|
|
||||||
role="button"
|
|
||||||
aria-expanded="false"
|
|
||||||
tabindex="0"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-controls="navbar-dropdown"
|
|
||||||
>
|
|
||||||
{% include 'snippets/avatar.html' with user=request.user %}
|
|
||||||
<span class="ml-2">{{ request.user.display_name }}</span>
|
|
||||||
</a>
|
|
||||||
<ul class="navbar-dropdown" id="navbar_dropdown">
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'directory' %}" class="navbar-item">
|
|
||||||
{% trans "Directory" %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'user-shelves' request.user.localname %}" class="navbar-item">
|
|
||||||
{% trans 'Your Books' %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'direct-messages' %}" class="navbar-item">
|
|
||||||
{% trans "Direct Messages" %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'prefs-profile' %}" class="navbar-item">
|
|
||||||
{% trans 'Settings' %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% if perms.bookwyrm.create_invites or perms.moderate_user %}
|
|
||||||
<li class="navbar-divider" role="presentation"> </li>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.bookwyrm.create_invites and not site.allow_registration %}
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'settings-invite-requests' %}" class="navbar-item">
|
|
||||||
{% trans 'Invites' %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if perms.bookwyrm.moderate_user %}
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'settings-dashboard' %}" class="navbar-item">
|
|
||||||
{% trans 'Admin' %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
<li class="navbar-divider" role="presentation"> </li>
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'logout' %}" class="navbar-item">
|
|
||||||
{% trans 'Log out' %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-item mt-3 py-0">
|
<div class="navbar-item mt-3 py-0">
|
||||||
<a href="{% url 'notifications' %}" class="tags has-addons">
|
<a href="{% url 'notifications' %}" class="tags has-addons">
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{% url 'lists' %}">{% trans "Lists" %}</a></li>
|
<li><a href="{% url 'lists' %}">{% trans "Lists" %}</a></li>
|
||||||
<li><a href="{% url 'list' list.id %}">{{ list.name|truncatechars:30 }}</a></li>
|
<li><a href="{% url 'list' list_id=list.id slug=list.name|slugify %}">{{ list.name|truncatechars:30 }}</a></li>
|
||||||
<li class="is-active">
|
<li class="is-active">
|
||||||
<a href="#" aria-current="page">
|
<a href="#" aria-current="page">
|
||||||
{% trans "Curate" %}
|
{% trans "Curate" %}
|
||||||
|
|
|
@ -180,7 +180,7 @@
|
||||||
<h2 class="title is-5">
|
<h2 class="title is-5">
|
||||||
{% trans "Sort List" %}
|
{% trans "Sort List" %}
|
||||||
</h2>
|
</h2>
|
||||||
<form name="sort" action="{% url 'list' list.id %}" method="GET" class="block">
|
<form name="sort" action="{% url 'list' list_id=list.id slug=list.name|slugify %}" method="GET" class="block">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_sort_by">{% trans "Sort By" %}</label>
|
<label class="label" for="id_sort_by">{% trans "Sort By" %}</label>
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
|
@ -207,7 +207,7 @@
|
||||||
{% trans "Suggest Books" %}
|
{% trans "Suggest Books" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
<form name="search" action="{% url 'list' list.id %}" method="GET" class="block">
|
<form name="search" action="{% url 'list' list_id=list.id slug=list.name|slugify %}" method="GET" class="block">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input aria-label="{% trans 'Search for a book' %}" class="input" type="text" name="q" placeholder="{% trans '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 }}">
|
||||||
|
@ -221,7 +221,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if query %}
|
{% if query %}
|
||||||
<p class="help"><a href="{% url 'list' list.id %}">{% trans "Clear search" %}</a></p>
|
<p class="help"><a href="{% url 'list' list_id=list.id slug=list.name|slugify %}">{% trans "Clear search" %}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
{% if not suggested_books %}
|
{% if not suggested_books %}
|
||||||
|
|
|
@ -47,12 +47,12 @@
|
||||||
|
|
||||||
|
|
||||||
{% block preview %}
|
{% block preview %}
|
||||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-grey-dark{% endif %}">
|
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-muted{% endif %}">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-clipped">
|
<div class="column is-clipped">
|
||||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow has-grey-dark">
|
<div class="column is-narrow has-text-muted">
|
||||||
{{ related_status.published_date|timesince }}
|
{{ related_status.published_date|timesince }}
|
||||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -47,12 +47,12 @@
|
||||||
|
|
||||||
|
|
||||||
{% block preview %}
|
{% block preview %}
|
||||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-grey-dark{% endif %}">
|
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-muted{% endif %}">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-clipped">
|
<div class="column is-clipped">
|
||||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow has-grey-dark">
|
<div class="column is-narrow has-text-muted">
|
||||||
{{ related_status.published_date|timesince }}
|
{{ related_status.published_date|timesince }}
|
||||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% load notification_page_tags %}
|
{% load notification_page_tags %}
|
||||||
{% related_status notification as related_status %}
|
{% related_status notification as related_status %}
|
||||||
<div class="notification {% if notification.id in unread %}has-background-primary{% endif %}">
|
<div class="notification {% if notification.id in unread %}has-background-primary{% endif %}">
|
||||||
<div class="columns is-mobile {% if notification.id in unread %}has-text-white{% else %}has-text-grey{% endif %}">
|
<div class="columns is-mobile {% if notification.id in unread %}has-text-white{% else %}has-text-more-muted{% endif %}">
|
||||||
<div class="column is-narrow is-size-3">
|
<div class="column is-narrow is-size-3">
|
||||||
<a class="icon" href="{% block primary_link %}{% endblock %}">
|
<a class="icon" href="{% block primary_link %}{% endblock %}">
|
||||||
{% block icon %}{% endblock %}
|
{% block icon %}{% endblock %}
|
||||||
|
|
|
@ -48,12 +48,12 @@
|
||||||
|
|
||||||
|
|
||||||
{% block preview %}
|
{% block preview %}
|
||||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-black{% endif %}">
|
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-default{% endif %}">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-clipped">
|
<div class="column is-clipped">
|
||||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow has-text-black">
|
<div class="column is-narrow has-text-default">
|
||||||
{{ related_status.published_date|timesince }}
|
{{ related_status.published_date|timesince }}
|
||||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -51,12 +51,12 @@
|
||||||
|
|
||||||
|
|
||||||
{% block preview %}
|
{% block preview %}
|
||||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-black{% endif %}">
|
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-body has-text-default{% endif %}">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-clipped">
|
<div class="column is-clipped">
|
||||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow has-text-black">
|
<div class="column is-narrow has-text-default">
|
||||||
{{ related_status.published_date|timesince }}
|
{{ related_status.published_date|timesince }}
|
||||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||||
</div>
|
</div>
|
||||||
|
|
22
bookwyrm/templates/preferences/export.html
Normal file
22
bookwyrm/templates/preferences/export.html
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends 'preferences/layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "CSV Export" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
{% trans "CSV Export" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
<div class="block content">
|
||||||
|
<p class="notification">
|
||||||
|
{% trans "Your export will include all the books on your shelves, books you have reviewed, and books with reading activity." %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'prefs-export-file' %}" class="button">
|
||||||
|
<span class="icon icon-download" aria-hidden="true"></span>
|
||||||
|
<span>Download file</span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -24,6 +24,17 @@
|
||||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Delete Account" %}</a>
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Delete Account" %}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<h2 class="menu-label">{% trans "Data" %}</h2>
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li>
|
||||||
|
{% url 'import' as url %}
|
||||||
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Import" %}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{% url 'prefs-export' as url %}
|
||||||
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "CSV export" %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<h2 class="menu-label">{% trans "Relationships" %}</h2>
|
<h2 class="menu-label">{% trans "Relationships" %}</h2>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<li>
|
||||||
|
|
14
bookwyrm/templates/reading_progress/stop.html
Normal file
14
bookwyrm/templates/reading_progress/stop.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% blocktrans trimmed with book_title=book.title %}
|
||||||
|
Stop Reading "{{ book_title }}"
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% include "snippets/reading_modals/stop_reading_modal.html" with book=book active=True static=True %}
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -19,6 +19,7 @@
|
||||||
</label>
|
</label>
|
||||||
{% include "snippets/progress_field.html" with id=field_id %}
|
{% include "snippets/progress_field.html" with id=field_id %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_finish_date_{{ readthrough.id }}">
|
<label class="label" for="id_finish_date_{{ readthrough.id }}">
|
||||||
{% trans "Finished reading" %}
|
{% trans "Finished reading" %}
|
||||||
|
|
|
@ -8,10 +8,12 @@
|
||||||
<div class="column">
|
<div class="column">
|
||||||
{% trans "Progress Updates:" %}
|
{% trans "Progress Updates:" %}
|
||||||
<ul>
|
<ul>
|
||||||
{% if readthrough.finish_date or readthrough.progress %}
|
{% if readthrough.finish_date or readthrough.stopped_date or readthrough.progress %}
|
||||||
<li>
|
<li>
|
||||||
{% if readthrough.finish_date %}
|
{% if readthrough.finish_date %}
|
||||||
{{ readthrough.finish_date | localtime | naturalday }}: {% trans "finished" %}
|
{{ readthrough.finish_date | localtime | naturalday }}: {% trans "finished" %}
|
||||||
|
{% elif readthrough.stopped_date %}
|
||||||
|
{{ readthrough.stopped_date | localtime | naturalday }}: {% trans "stopped" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
{% if readthrough.progress_mode == 'PG' %}
|
{% if readthrough.progress_mode == 'PG' %}
|
||||||
|
|
|
@ -17,7 +17,14 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal-form-open %}
|
{% block modal-form-open %}
|
||||||
<form name="add-readthrough-{{ readthrough.id }}" action="/create-readthrough" method="post">
|
<form
|
||||||
|
name="add-readthrough-{{ readthrough.id }}"
|
||||||
|
{% if readthrough.id %}
|
||||||
|
action="{% url 'edit-readthrough' %}"
|
||||||
|
{% else %}
|
||||||
|
action="{% url 'create-readthrough' %}"
|
||||||
|
{% endif %}
|
||||||
|
method="POST">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal-body %}
|
{% block modal-body %}
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
{% if result_set.results %}
|
{% if result_set.results %}
|
||||||
<section class="mb-5">
|
<section class="mb-5">
|
||||||
{% if not result_set.connector.local %}
|
{% if not result_set.connector.local %}
|
||||||
<details class="details-panel box" {% if forloop.first %}open{% endif %}>
|
<details class="details-panel box" open>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not result_set.connector.local %}
|
{% if not result_set.connector.local %}
|
||||||
<summary class="is-flex is-align-items-center is-flex-wrap-wrap is-gap-2">
|
<summary class="is-flex is-align-items-center is-flex-wrap-wrap is-gap-2">
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
<div class="block table-container">
|
<div class="block table-container">
|
||||||
<table class="table is-striped">
|
<table class="table is-striped is-fullwidth">
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
{% url 'settings-announcements' as url %}
|
{% url 'settings-announcements' as url %}
|
||||||
|
|
|
@ -154,7 +154,7 @@
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="table is-striped">
|
<table class="table is-striped is-fullwidth">
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<label for="id_string_match">{% trans "String match" %}</label>
|
<label for="id_string_match">{% trans "String match" %}</label>
|
||||||
|
|
|
@ -10,26 +10,26 @@
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
<div class="columns block has-text-centered is-mobile is-multiline">
|
<div class="columns block has-text-centered is-mobile is-multiline">
|
||||||
<div class="column is-3-desktop is-6-mobile">
|
<div class="column is-3-desktop is-6-mobile is-flex">
|
||||||
<div class="notification">
|
<div class="notification is-flex-grow-1">
|
||||||
<h3>{% trans "Total users" %}</h3>
|
<h3>{% trans "Total users" %}</h3>
|
||||||
<p class="title is-5">{{ users|intcomma }}</p>
|
<p class="title is-5">{{ users|intcomma }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3-desktop is-6-mobile">
|
<div class="column is-3-desktop is-6-mobil is-flexe">
|
||||||
<div class="notification">
|
<div class="notification is-flex-grow-1">
|
||||||
<h3>{% trans "Active this month" %}</h3>
|
<h3>{% trans "Active this month" %}</h3>
|
||||||
<p class="title is-5">{{ active_users|intcomma }}</p>
|
<p class="title is-5">{{ active_users|intcomma }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3-desktop is-6-mobile">
|
<div class="column is-3-desktop is-6-mobile is-flex">
|
||||||
<div class="notification">
|
<div class="notification is-flex-grow-1">
|
||||||
<h3>{% trans "Statuses" %}</h3>
|
<h3>{% trans "Statuses" %}</h3>
|
||||||
<p class="title is-5">{{ statuses|intcomma }}</p>
|
<p class="title is-5">{{ statuses|intcomma }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-3-desktop is-6-mobile">
|
<div class="column is-3-desktop is-6-mobile is-flex">
|
||||||
<div class="notification">
|
<div class="notification is-flex-grow-1">
|
||||||
<h3>{% trans "Works" %}</h3>
|
<h3>{% trans "Works" %}</h3>
|
||||||
<p class="title is-5">{{ works|intcomma }}</p>
|
<p class="title is-5">{{ works|intcomma }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,8 +38,8 @@
|
||||||
|
|
||||||
<div class="columns block is-multiline">
|
<div class="columns block is-multiline">
|
||||||
{% if reports %}
|
{% if reports %}
|
||||||
<div class="column">
|
<div class="column is-flex">
|
||||||
<a href="{% url 'settings-reports' %}" class="notification is-warning is-block">
|
<a href="{% url 'settings-reports' %}" class="notification is-warning is-block is-flex-grow-1">
|
||||||
{% blocktrans trimmed count counter=reports with display_count=reports|intcomma %}
|
{% blocktrans trimmed count counter=reports with display_count=reports|intcomma %}
|
||||||
{{ display_count }} open report
|
{{ display_count }} open report
|
||||||
{% plural %}
|
{% plural %}
|
||||||
|
@ -50,8 +50,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if pending_domains %}
|
{% if pending_domains %}
|
||||||
<div class="column">
|
<div class="column is-flex">
|
||||||
<a href="{% url 'settings-link-domain' %}" class="notification is-primary is-block">
|
<a href="{% url 'settings-link-domain' %}" class="notification is-primary is-block is-flex-grow-1">
|
||||||
{% blocktrans trimmed count counter=pending_domains with display_count=pending_domains|intcomma %}
|
{% blocktrans trimmed count counter=pending_domains with display_count=pending_domains|intcomma %}
|
||||||
{{ display_count }} domain needs review
|
{{ display_count }} domain needs review
|
||||||
{% plural %}
|
{% plural %}
|
||||||
|
@ -62,8 +62,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not site.allow_registration and site.allow_invite_requests and invite_requests %}
|
{% if not site.allow_registration and site.allow_invite_requests and invite_requests %}
|
||||||
<div class="column">
|
<div class="column is-flex">
|
||||||
<a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success">
|
<a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success is-flex-grow-1">
|
||||||
{% blocktrans trimmed count counter=invite_requests with display_count=invite_requests|intcomma %}
|
{% blocktrans trimmed count counter=invite_requests with display_count=invite_requests|intcomma %}
|
||||||
{{ display_count }} invite request
|
{{ display_count }} invite request
|
||||||
{% plural %}
|
{% plural %}
|
||||||
|
@ -74,8 +74,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if current_version %}
|
{% if current_version %}
|
||||||
<div class="column">
|
<div class="column is-flex">
|
||||||
<a href="https://docs.joinbookwyrm.com/updating-your-instance.html" class="notification is-block is-warning" target="_blank">
|
<a href="https://docs.joinbookwyrm.com/updating-your-instance.html" class="notification is-block is-warning is-flex-grow-1" target="_blank">
|
||||||
{% blocktrans trimmed with current=current_version available=available_version %}
|
{% blocktrans trimmed with current=current_version available=available_version %}
|
||||||
An update is available! You're running v{{ current }} and the latest release is {{ available }}.
|
An update is available! You're running v{{ current }} and the latest release is {{ available }}.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
||||||
|
|
||||||
|
{% block filter_fields %}
|
||||||
|
{% include 'settings/federation/software_filter.html' %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
|
{% include 'settings/federation/instance_filters.html' %}
|
||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<ul>
|
<ul>
|
||||||
{% url 'settings-federation' status='federated' as url %}
|
{% url 'settings-federation' status='federated' as url %}
|
||||||
|
@ -25,7 +28,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table is-striped">
|
<table class="table is-striped is-fullwidth">
|
||||||
<tr>
|
<tr>
|
||||||
{% url 'settings-federation' as url %}
|
{% url 'settings-federation' as url %}
|
||||||
<th>
|
<th>
|
||||||
|
@ -36,6 +39,10 @@
|
||||||
{% trans "Date added" as text %}
|
{% trans "Date added" as text %}
|
||||||
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
|
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
|
||||||
</th>
|
</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Last updated" as text %}
|
||||||
|
{% include 'snippets/table-sort-header.html' with field="updated_date" sort=sort text=text %}
|
||||||
|
</th>
|
||||||
<th>
|
<th>
|
||||||
{% trans "Software" as text %}
|
{% trans "Software" as text %}
|
||||||
{% include 'snippets/table-sort-header.html' with field="application_type" sort=sort text=text %}
|
{% include 'snippets/table-sort-header.html' with field="application_type" sort=sort text=text %}
|
||||||
|
@ -43,12 +50,12 @@
|
||||||
<th>
|
<th>
|
||||||
{% trans "Users" %}
|
{% trans "Users" %}
|
||||||
</th>
|
</th>
|
||||||
<th>{% trans "Status" %}</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% for server in servers %}
|
{% for server in servers %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{% url 'settings-federated-server' server.id %}">{{ server.server_name }}</a></td>
|
<td><a href="{% url 'settings-federated-server' server.id %}">{{ server.server_name }}</a></td>
|
||||||
<td>{{ server.created_date }}</td>
|
<td>{{ server.created_date|date:'Y-m-d' }}</td>
|
||||||
|
<td>{{ server.updated_date|date:'Y-m-d' }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if server.application_type %}
|
{% if server.application_type %}
|
||||||
{{ server.application_type }}
|
{{ server.application_type }}
|
||||||
|
@ -56,7 +63,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ server.user_set.count }}</td>
|
<td>{{ server.user_set.count }}</td>
|
||||||
<td>{{ server.get_status_display }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if not servers %}
|
{% if not servers %}
|
||||||
|
|
19
bookwyrm/templates/settings/federation/software_filter.html
Normal file
19
bookwyrm/templates/settings/federation/software_filter.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends 'snippets/filters_panel/filter_field.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block filter %}
|
||||||
|
<label class="label" for="id_server">{% trans "Software" %}</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select name="application_type">
|
||||||
|
<option value="">-----</option>
|
||||||
|
{% for option in software_options %}
|
||||||
|
{% if option %}
|
||||||
|
<option value="{{ option }}">{{ option }}</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<input type="text" name="address" maxlength="255" class="input" required="" id="id_address" placeholder="190.0.2.0/24" aria-describedby="desc_address">
|
<input type="text" name="address" maxlength="255" class="input" required="" id="id_address" placeholder="190.0.2.0/24" aria-describedby="desc_address">
|
||||||
|
<p class="help">{% trans "You can block IP ranges using CIDR syntax." %}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'snippets/form_errors.html' with errors_list=form.address.errors id="desc_address" %}
|
{% include 'snippets/form_errors.html' with errors_list=form.address.errors id="desc_address" %}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
{% extends 'components/tooltip.html' %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block tooltip_content %}
|
|
||||||
|
|
||||||
{% trans "You can block IP ranges using CIDR syntax." %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -93,7 +93,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
<div class="column">
|
<div class="column is-clipped">
|
||||||
{% block panel %}{% endblock %}
|
{% block panel %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,5 +44,6 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% include 'snippets/pagination.html' with page=reports path=request.path %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -139,6 +139,13 @@
|
||||||
{% trans "Allow registration" %}
|
{% trans "Allow registration" %}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label mb-0" for="id_require_confirm_email">
|
||||||
|
{{ site_form.require_confirm_email }}
|
||||||
|
{% trans "Require users to confirm email address" %}
|
||||||
|
</label>
|
||||||
|
<p class="help" id="desc_require_confirm_email">{% trans "(Recommended if registration is open)" %}</p>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_allow_invite_requests">
|
<label class="label" for="id_allow_invite_requests">
|
||||||
{{ site_form.allow_invite_requests }}
|
{{ site_form.allow_invite_requests }}
|
||||||
|
@ -157,13 +164,6 @@
|
||||||
{{ site_form.invite_question_text }}
|
{{ site_form.invite_question_text }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<label class="label mb-0" for="id_require_confirm_email">
|
|
||||||
{{ site_form.require_confirm_email }}
|
|
||||||
{% trans "Require users to confirm email address" %}
|
|
||||||
</label>
|
|
||||||
<p class="help" id="desc_require_confirm_email">{% trans "(Recommended if registration is open)" %}</p>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
|
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
|
||||||
{{ site_form.registration_closed_text }}
|
{{ site_form.registration_closed_text }}
|
||||||
|
@ -171,7 +171,7 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
|
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
|
||||||
{{ site_form.invite_request_text }}
|
{{ site_form.invite_request_text }}
|
||||||
|
|
||||||
{% include 'snippets/form_errors.html' with errors_list=site_form.invite_request_text.errors id="desc_invite_request_text" %}
|
{% include 'snippets/form_errors.html' with errors_list=site_form.invite_request_text.errors id="desc_invite_request_text" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
<section class="block content">
|
<section class="block content">
|
||||||
<h2 class="title is-4">{% trans "Available Themes" %}</h2>
|
<h2 class="title is-4">{% trans "Available Themes" %}</h2>
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="table is-striped">
|
<table class="table is-striped is-fullwidth">
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
{% trans "Theme name" %}
|
{% trans "Theme name" %}
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-container block">
|
<div class="table-container block">
|
||||||
<table class="table is-striped">
|
<table class="table is-striped is-fullwidth">
|
||||||
<tr>
|
<tr>
|
||||||
{% url 'settings-users' as url %}
|
{% url 'settings-users' as url %}
|
||||||
<th>
|
<th>
|
||||||
|
@ -61,10 +61,25 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{% url 'settings-user' user.id %}">{{ user|username }}</a></td>
|
<td class="overflow-wrap-anywhere">
|
||||||
|
<a href="{% url 'settings-user' user.id %}">{{ user|username }}</a>
|
||||||
|
</td>
|
||||||
<td>{{ user.created_date }}</td>
|
<td>{{ user.created_date }}</td>
|
||||||
<td>{{ user.last_active_date }}</td>
|
<td>{{ user.last_active_date }}</td>
|
||||||
<td>{% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}</td>
|
<td>
|
||||||
|
{% if user.is_active %}
|
||||||
|
<span class="tag is-success" aria-hidden="true">
|
||||||
|
<span class="icon icon-check"></span>
|
||||||
|
</span>
|
||||||
|
{% trans "Active" %}
|
||||||
|
{% else %}
|
||||||
|
<span class="tag is-warning" aria-hidden="true">
|
||||||
|
<span class="icon icon-x"></span>
|
||||||
|
</span>
|
||||||
|
{% trans "Inactive" %}
|
||||||
|
<span class="help">({{ user.get_deactivation_reason_display }})</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
{% if status != "local" %}
|
{% if status != "local" %}
|
||||||
<td>
|
<td>
|
||||||
{% if user.federated_server %}
|
{% if user.federated_server %}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="column is-flex is-flex-direction-column">
|
<div class="column is-flex is-flex-direction-column">
|
||||||
<h4 class="title is-4">{% trans "Profile" %}</h4>
|
<h4 class="title is-4">{% trans "Profile" %}</h4>
|
||||||
<div class="box is-flex-grow-1">
|
<div class="box is-flex-grow-1">
|
||||||
{% include 'user/user_preview.html' with user=user %}
|
{% include 'user/user_preview.html' with user=user admin_mode=True %}
|
||||||
{% if user.summary %}
|
{% if user.summary %}
|
||||||
<div class="box content has-background-secondary is-shadowless">
|
<div class="box content has-background-secondary is-shadowless">
|
||||||
{{ user.summary|to_markdown|safe }}
|
{{ user.summary|to_markdown|safe }}
|
||||||
|
@ -14,6 +14,10 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p class="mt-2"><a href="{{ user.local_path }}">{% trans "View user profile" %}</a></p>
|
<p class="mt-2"><a href="{{ user.local_path }}">{% trans "View user profile" %}</a></p>
|
||||||
|
{% url 'settings-user' user.id as url %}
|
||||||
|
{% if not request.path == url %}
|
||||||
|
<p class="mt-2"><a href="{{ url }}">{% trans "Go to user admin" %}</a></p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-flex is-flex-direction-column is-4">
|
<div class="column is-flex is-flex-direction-column is-4">
|
||||||
|
@ -67,6 +71,9 @@
|
||||||
<dt class="is-pulled-left mr-5">{% trans "Blocked by count:" %}</dt>
|
<dt class="is-pulled-left mr-5">{% trans "Blocked by count:" %}</dt>
|
||||||
<dd>{{ user.blocked_by.count }}</dd>
|
<dd>{{ user.blocked_by.count }}</dd>
|
||||||
|
|
||||||
|
<dt class="is-pulled-left mr-5">{% trans "Date added:" %}</dt>
|
||||||
|
<dd>{{ user.created_date }}</dd>
|
||||||
|
|
||||||
<dt class="is-pulled-left mr-5">{% trans "Last active date:" %}</dt>
|
<dt class="is-pulled-left mr-5">{% trans "Last active date:" %}</dt>
|
||||||
<dd>{{ user.last_active_date }}</dd>
|
<dd>{{ user.last_active_date }}</dd>
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,7 @@
|
||||||
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
|
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
|
||||||
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
|
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
|
||||||
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
|
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
|
||||||
|
{% elif shelf.identifier == 'stopped-reading' %}{% trans "Stopped Reading" %}
|
||||||
{% else %}{{ shelf.name }}{% endif %}
|
{% else %}{{ shelf.name }}{% endif %}
|
||||||
<span class="subtitle">
|
<span class="subtitle">
|
||||||
{% include 'snippets/privacy-icons.html' with item=shelf %}
|
{% include 'snippets/privacy-icons.html' with item=shelf %}
|
||||||
|
@ -150,7 +151,7 @@
|
||||||
{% if is_self %}
|
{% if is_self %}
|
||||||
<th>{% trans "Shelved" as text %}{% include 'snippets/table-sort-header.html' with field="shelved_date" sort=sort text=text %}</th>
|
<th>{% trans "Shelved" as text %}{% include 'snippets/table-sort-header.html' with field="shelved_date" sort=sort text=text %}</th>
|
||||||
<th>{% trans "Started" as text %}{% include 'snippets/table-sort-header.html' with field="start_date" sort=sort text=text %}</th>
|
<th>{% trans "Started" as text %}{% include 'snippets/table-sort-header.html' with field="start_date" sort=sort text=text %}</th>
|
||||||
<th>{% trans "Finished" as text %}{% include 'snippets/table-sort-header.html' with field="finish_date" sort=sort text=text %}</th>
|
<th>{% if shelf.identifier == 'read' %}{% trans "Finished" as text %}{% else %}{% trans "Until" as text %}{% endif %}{% include 'snippets/table-sort-header.html' with field="finish_date" sort=sort text=text %}</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th>{% trans "Rating" as text %}{% include 'snippets/table-sort-header.html' with field="rating" sort=sort text=text %}</th>
|
<th>{% trans "Rating" as text %}{% include 'snippets/table-sort-header.html' with field="rating" sort=sort text=text %}</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -180,7 +181,7 @@
|
||||||
<td data-title="{% trans "Started" %}">
|
<td data-title="{% trans "Started" %}">
|
||||||
{{ book.start_date|naturalday|default_if_none:""}}
|
{{ book.start_date|naturalday|default_if_none:""}}
|
||||||
</td>
|
</td>
|
||||||
<td data-title="{% trans "Finished" %}">
|
<td data-title="{% if shelf.identifier == 'read' %}{% trans "Finished" as text %}{% else %}{% trans "Until" as text %}{% endif %}">
|
||||||
{{ book.finish_date|naturalday|default_if_none:""}}
|
{{ book.finish_date|naturalday|default_if_none:""}}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -14,6 +14,11 @@
|
||||||
{% blocktrans with username=goal.user.display_name read_count=progress.count|intcomma goal_count=goal.goal|intcomma path=goal.local_path %}{{ username }} has read <a href="{{ path }}">{{ read_count }} of {{ goal_count}} books</a>.{% endblocktrans %}
|
{% blocktrans with username=goal.user.display_name read_count=progress.count|intcomma goal_count=goal.goal|intcomma path=goal.local_path %}{{ username }} has read <a href="{{ path }}">{{ read_count }} of {{ goal_count}} books</a>.{% endblocktrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<progress class="progress is-large" value="{{ progress.count }}" max="{{ goal.goal }}" aria-hidden="true">{{ progress.percent }}%</progress>
|
<progress
|
||||||
|
class="progress is-large is-primary"
|
||||||
|
value="{{ progress.count }}"
|
||||||
|
max="{{ goal.goal }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
>{{ progress.percent }}%</progress>
|
||||||
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
<form name="reading-progress-{{ uuid }}" action="{% url 'reading-status-update' book.id %}" method="POST" class="submit-status">
|
<form name="reading-progress-{{ uuid }}" action="{% url 'reading-status-update' book.id %}" method="POST" class="submit-status">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
||||||
|
<input type="hidden" name="start_date" value="{{ readthrough.start_date|date:'Y-m-d' }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block reading-dates %}
|
{% block reading-dates %}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
{% extends 'snippets/reading_modals/layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
|
||||||
|
{% block modal-title %}
|
||||||
|
{% blocktrans trimmed with book_title=book|book_title %}
|
||||||
|
Stop Reading "<em>{{ book_title }}</em>"
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-form-open %}
|
||||||
|
<form name="stop-reading-{{ uuid }}" action="{% url 'reading-status' 'stop' book.id %}" method="post" {% if not refresh %}class="submit-status"{% endif %}>
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
||||||
|
<input type="hidden" name="reading_status" value="stopped-reading">
|
||||||
|
<input type="hidden" name="shelf" value="{{ move_from }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block reading-dates %}
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-half">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="stop_id_start_date_{{ uuid }}">
|
||||||
|
{% trans "Started reading" %}
|
||||||
|
</label>
|
||||||
|
<input type="date" name="start_date" class="input" id="stop_id_start_date_{{ uuid }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-half">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="id_read_until_date_{{ uuid }}">
|
||||||
|
{% trans "Stopped reading" %}
|
||||||
|
</label>
|
||||||
|
<input type="date" name="stopped_date" class="input" id="id_read_until_date_{{ uuid }}" value="{% now "Y-m-d" %}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
{% include "snippets/reading_modals/form.html" with optional=True type="stop_modal" %}
|
||||||
|
{% endblock %}
|
|
@ -49,6 +49,13 @@
|
||||||
{% join "finish_reading" uuid as modal_id %}
|
{% join "finish_reading" uuid as modal_id %}
|
||||||
{% include 'snippets/shelve_button/modal_button.html' with class=button_class fallback_url=fallback_url %}
|
{% include 'snippets/shelve_button/modal_button.html' with class=button_class fallback_url=fallback_url %}
|
||||||
|
|
||||||
|
{% elif shelf.identifier == 'stopped-reading' %}
|
||||||
|
|
||||||
|
{% trans "Stopped reading" as button_text %}
|
||||||
|
{% url 'reading-status' 'stop' book.id as fallback_url %}
|
||||||
|
{% join "stop_reading" uuid as modal_id %}
|
||||||
|
{% include 'snippets/shelve_button/modal_button.html' with class=button_class fallback_url=fallback_url %}
|
||||||
|
|
||||||
{% elif shelf.identifier == 'to-read' %}
|
{% elif shelf.identifier == 'to-read' %}
|
||||||
|
|
||||||
{% trans "Want to read" as button_text %}
|
{% trans "Want to read" as button_text %}
|
||||||
|
@ -99,5 +106,8 @@
|
||||||
{% join "finish_reading" uuid as modal_id %}
|
{% join "finish_reading" uuid as modal_id %}
|
||||||
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book id=modal_id move_from=current.id readthrough=readthrough refresh=True class="" %}
|
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book id=modal_id move_from=current.id readthrough=readthrough refresh=True class="" %}
|
||||||
|
|
||||||
|
{% join "stop_reading" uuid as modal_id %}
|
||||||
|
{% include 'snippets/reading_modals/stop_reading_modal.html' with book=active_shelf.book id=modal_id move_from=current.id readthrough=readthrough refresh=True class="" %}
|
||||||
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -29,6 +29,9 @@
|
||||||
{% join "finish_reading" uuid as modal_id %}
|
{% join "finish_reading" uuid as modal_id %}
|
||||||
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
|
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
|
||||||
|
|
||||||
|
{% join "stop_reading" uuid as modal_id %}
|
||||||
|
{% include 'snippets/reading_modals/stop_reading_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
|
||||||
|
|
||||||
{% join "progress_update" uuid as modal_id %}
|
{% join "progress_update" uuid as modal_id %}
|
||||||
{% include 'snippets/reading_modals/progress_update_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
|
{% include 'snippets/reading_modals/progress_update_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
|
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
|
||||||
<li role="menuitem" class="dropdown-item p-0">
|
<li role="menuitem" class="dropdown-item p-0">
|
||||||
<div
|
<div
|
||||||
class="{% if next_shelf_identifier == shelf.identifier %}is-hidden{% endif %}"
|
class="{% if is_current or next_shelf_identifier == shelf.identifier %}is-hidden{% elif shelf.identifier == 'stopped-reading' and active_shelf.shelf.identifier != "reading" %}is-hidden{% endif %}"
|
||||||
data-shelf-dropdown-identifier="{{ shelf.identifier }}"
|
data-shelf-dropdown-identifier="{{ shelf.identifier }}"
|
||||||
data-shelf-next="{{ shelf.identifier|next_shelf }}"
|
data-shelf-next="{{ shelf.identifier|next_shelf }}"
|
||||||
>
|
>
|
||||||
|
@ -26,6 +26,13 @@
|
||||||
{% join "finish_reading" button_uuid as modal_id %}
|
{% join "finish_reading" button_uuid as modal_id %}
|
||||||
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
|
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
|
||||||
|
|
||||||
|
{% elif shelf.identifier == 'stopped-reading' %}
|
||||||
|
|
||||||
|
{% trans "Stop reading" as button_text %}
|
||||||
|
{% url 'reading-status' 'stop' book.id as fallback_url %}
|
||||||
|
{% join "stop_reading" button_uuid as modal_id %}
|
||||||
|
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
|
||||||
|
|
||||||
{% elif shelf.identifier == 'to-read' %}
|
{% elif shelf.identifier == 'to-read' %}
|
||||||
|
|
||||||
{% trans "Want to read" as button_text %}
|
{% trans "Want to read" as button_text %}
|
||||||
|
|
|
@ -13,6 +13,15 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="{% if next_shelf_identifier != 'stopped-reading-complete' %}is-hidden{% endif %}"
|
||||||
|
data-shelf-identifier="stopped-reading-complete"
|
||||||
|
>
|
||||||
|
<button type="button" class="button {{ class }}" disabled>
|
||||||
|
<span>{% trans "Stopped reading" %}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% for shelf in shelves %}
|
{% for shelf in shelves %}
|
||||||
<div
|
<div
|
||||||
class="{% if next_shelf_identifier != shelf.identifier %}is-hidden{% endif %}"
|
class="{% if next_shelf_identifier != shelf.identifier %}is-hidden{% endif %}"
|
||||||
|
@ -33,6 +42,14 @@
|
||||||
{% join "finish_reading" button_uuid as modal_id %}
|
{% join "finish_reading" button_uuid as modal_id %}
|
||||||
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
|
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
|
||||||
|
|
||||||
|
|
||||||
|
{% elif shelf.identifier == 'stopped-reading' %}
|
||||||
|
|
||||||
|
{% trans "Stop reading" as button_text %}
|
||||||
|
{% url 'reading-status' 'finish' book.id as fallback_url %}
|
||||||
|
{% join "stop_reading" button_uuid as modal_id %}
|
||||||
|
{% include 'snippets/shelve_button/modal_button.html' with class=class fallback_url=fallback_url %}
|
||||||
|
|
||||||
{% elif shelf.identifier == 'to-read' %}
|
{% elif shelf.identifier == 'to-read' %}
|
||||||
|
|
||||||
{% trans "Want to read" as button_text %}
|
{% trans "Want to read" as button_text %}
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<article class="column ml-3-tablet my-3-mobile">
|
<article class="column ml-3-tablet my-3-mobile is-clipped">
|
||||||
{% if status_type == 'Review' %}
|
{% if status_type == 'Review' %}
|
||||||
<header class="mb-2">
|
<header class="mb-2">
|
||||||
<h3
|
<h3
|
||||||
|
@ -112,6 +112,9 @@
|
||||||
{% with full=status.content|safe no_trim=status.content_warning itemprop="reviewBody" %}
|
{% with full=status.content|safe no_trim=status.content_warning itemprop="reviewBody" %}
|
||||||
{% include 'snippets/trimmed_text.html' %}
|
{% include 'snippets/trimmed_text.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
{% if status.progress %}
|
||||||
|
<div class="is-small is-italic has-text-right mr-3">{% trans "page" %} {{ status.progress }}</div>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if status.attachments.exists %}
|
{% if status.attachments.exists %}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue