Merge branch 'main' into bw-dev-npm-fix
This commit is contained in:
commit
51f445bc72
220 changed files with 17881 additions and 4815 deletions
|
@ -1,6 +1,7 @@
|
|||
""" basics for an activitypub serializer """
|
||||
from dataclasses import dataclass, fields, MISSING
|
||||
from json import JSONEncoder
|
||||
import logging
|
||||
|
||||
from django.apps import apps
|
||||
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.tasks import app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ActivitySerializerError(ValueError):
|
||||
"""routine problems serializing activitypub json"""
|
||||
|
@ -39,12 +42,12 @@ def naive_parse(activity_objects, activity_json, serializer=None):
|
|||
activity_json["type"] = "PublicKey"
|
||||
|
||||
activity_type = activity_json.get("type")
|
||||
if activity_type in ["Question", "Article"]:
|
||||
return None
|
||||
try:
|
||||
serializer = activity_objects[activity_type]
|
||||
except KeyError as err:
|
||||
# we know this exists and that we can't handle it
|
||||
if activity_type in ["Question"]:
|
||||
return None
|
||||
raise ActivitySerializerError(err)
|
||||
|
||||
return serializer(activity_objects=activity_objects, **activity_json)
|
||||
|
@ -65,7 +68,7 @@ class ActivityObject:
|
|||
try:
|
||||
value = kwargs[field.name]
|
||||
if value in (None, MISSING, {}):
|
||||
raise KeyError()
|
||||
raise KeyError("Missing required field", field.name)
|
||||
try:
|
||||
is_subclass = issubclass(field.type, ActivityObject)
|
||||
except TypeError:
|
||||
|
@ -268,9 +271,9 @@ def resolve_remote_id(
|
|||
try:
|
||||
data = get_data(remote_id)
|
||||
except ConnectorException:
|
||||
raise ActivitySerializerError(
|
||||
f"Could not connect to host for remote_id: {remote_id}"
|
||||
)
|
||||
logger.exception("Could not connect to host for remote_id: %s", remote_id)
|
||||
return None
|
||||
|
||||
# determine the model implicitly, if not provided
|
||||
# or if it's a model with subclasses like Status, check again
|
||||
if not model or hasattr(model.objects, "select_subclasses"):
|
||||
|
|
|
@ -298,8 +298,9 @@ def add_status_on_create_command(sender, instance, created):
|
|||
priority = HIGH
|
||||
# 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)
|
||||
one_day = 60 * 60 * 24
|
||||
if (instance.created_date - instance.published_date).seconds > one_day:
|
||||
if instance.published_date < timezone.now() - timedelta(
|
||||
days=1
|
||||
) or instance.created_date < instance.published_date - timedelta(days=1):
|
||||
priority = LOW
|
||||
|
||||
add_status_task.apply_async(
|
||||
|
|
|
@ -148,8 +148,8 @@ class SearchResult:
|
|||
|
||||
def __repr__(self):
|
||||
# pylint: disable=consider-using-f-string
|
||||
return "<SearchResult key={!r} title={!r} author={!r}>".format(
|
||||
self.key, self.title, self.author
|
||||
return "<SearchResult key={!r} title={!r} author={!r} confidence={!r}>".format(
|
||||
self.key, self.title, self.author, self.confidence
|
||||
)
|
||||
|
||||
def json(self):
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
""" functionality outline for a book data connector """
|
||||
from abc import ABC, abstractmethod
|
||||
import imghdr
|
||||
import ipaddress
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
import re
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import transaction
|
||||
|
@ -11,7 +10,7 @@ import requests
|
|||
from requests.exceptions import RequestException
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -39,62 +38,34 @@ class AbstractMinimalConnector(ABC):
|
|||
for field in self_fields:
|
||||
setattr(self, field, getattr(info, field))
|
||||
|
||||
def search(self, query, min_confidence=None, timeout=settings.QUERY_TIMEOUT):
|
||||
"""free text search"""
|
||||
params = {}
|
||||
if min_confidence:
|
||||
params["min_confidence"] = min_confidence
|
||||
def get_search_url(self, query):
|
||||
"""format the query url"""
|
||||
# Check if the query resembles an ISBN
|
||||
if maybe_isbn(query) and self.isbn_search_url and self.isbn_search_url != "":
|
||||
return f"{self.isbn_search_url}{query}"
|
||||
|
||||
data = self.get_search_data(
|
||||
f"{self.search_url}{query}",
|
||||
params=params,
|
||||
timeout=timeout,
|
||||
)
|
||||
results = []
|
||||
# NOTE: previously, we tried searching isbn and if that produces no results,
|
||||
# searched as free text. This, instead, only searches isbn if it's isbn-y
|
||||
return f"{self.search_url}{query}"
|
||||
|
||||
for doc in self.parse_search_data(data)[:10]:
|
||||
results.append(self.format_search_result(doc))
|
||||
return results
|
||||
|
||||
def isbn_search(self, query, timeout=settings.QUERY_TIMEOUT):
|
||||
"""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)
|
||||
def process_search_response(self, query, data, min_confidence):
|
||||
"""Format the search results based on the formt of the query"""
|
||||
if maybe_isbn(query):
|
||||
return list(self.parse_isbn_search_data(data))[:10]
|
||||
return list(self.parse_search_data(data, min_confidence))[:10]
|
||||
|
||||
@abstractmethod
|
||||
def get_or_create_book(self, remote_id):
|
||||
"""pull up a book record by whatever means possible"""
|
||||
|
||||
@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"""
|
||||
|
||||
@abstractmethod
|
||||
def format_search_result(self, search_result):
|
||||
"""create a SearchResult obj from json"""
|
||||
|
||||
@abstractmethod
|
||||
def parse_isbn_search_data(self, data):
|
||||
"""turn the result json from a search into a list"""
|
||||
|
||||
@abstractmethod
|
||||
def format_isbn_search_result(self, search_result):
|
||||
"""create a SearchResult obj from json"""
|
||||
|
||||
|
||||
class AbstractConnector(AbstractMinimalConnector):
|
||||
"""generic book data connector"""
|
||||
|
@ -254,9 +225,6 @@ def get_data(url, params=None, timeout=10):
|
|||
# check if the url is blocked
|
||||
raise_not_valid_url(url)
|
||||
|
||||
if models.FederatedServer.is_blocked(url):
|
||||
raise ConnectorException(f"Attempting to load data from blocked url: {url}")
|
||||
|
||||
try:
|
||||
resp = requests.get(
|
||||
url,
|
||||
|
@ -311,20 +279,6 @@ def get_image(url, timeout=10):
|
|||
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:
|
||||
"""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
|
||||
return None
|
||||
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):
|
||||
return activitypub.resolve_remote_id(remote_id, model=models.Edition)
|
||||
|
||||
def parse_search_data(self, data):
|
||||
return data
|
||||
|
||||
def format_search_result(self, search_result):
|
||||
search_result["connector"] = self
|
||||
return SearchResult(**search_result)
|
||||
def parse_search_data(self, data, min_confidence):
|
||||
for search_result in data:
|
||||
search_result["connector"] = self
|
||||
yield SearchResult(**search_result)
|
||||
|
||||
def parse_isbn_search_data(self, data):
|
||||
return data
|
||||
|
||||
def format_isbn_search_result(self, search_result):
|
||||
return self.format_search_result(search_result)
|
||||
for search_result in data:
|
||||
search_result["connector"] = self
|
||||
yield SearchResult(**search_result)
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
""" interface with whatever connectors the app has """
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
import importlib
|
||||
import ipaddress
|
||||
import logging
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import aiohttp
|
||||
from django.dispatch import receiver
|
||||
from django.db.models import signals
|
||||
|
||||
from requests import HTTPError
|
||||
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -21,53 +22,85 @@ class ConnectorException(HTTPError):
|
|||
"""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):
|
||||
"""find books based on arbitary keywords"""
|
||||
if not query:
|
||||
return []
|
||||
results = []
|
||||
|
||||
# Have we got a ISBN ?
|
||||
isbn = re.sub(r"[\W_]", "", query)
|
||||
maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13
|
||||
|
||||
start_time = datetime.now()
|
||||
items = []
|
||||
for connector in get_connectors():
|
||||
result_set = None
|
||||
if maybe_isbn and connector.isbn_search_url and connector.isbn_search_url != "":
|
||||
# Search on ISBN
|
||||
try:
|
||||
result_set = connector.isbn_search(isbn)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
logger.info(err)
|
||||
# if this fails, we can still try regular search
|
||||
# get the search url from the connector before sending
|
||||
url = connector.get_search_url(query)
|
||||
try:
|
||||
raise_not_valid_url(url)
|
||||
except ConnectorException:
|
||||
# if this URL is invalid we should skip it and move on
|
||||
logger.info("Request denied to blocked domain: %s", url)
|
||||
continue
|
||||
items.append((url, connector))
|
||||
|
||||
# if no isbn search results, we fallback to generic search
|
||||
if not result_set:
|
||||
try:
|
||||
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
|
||||
# load as many results as we can
|
||||
results = asyncio.run(async_connector_search(query, items, min_confidence))
|
||||
results = [r for r in results if r]
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -133,3 +166,20 @@ def create_connector(sender, instance, created, *args, **kwargs):
|
|||
"""create a connector to an external bookwyrm server"""
|
||||
if instance.application_type == "bookwyrm":
|
||||
get_or_create_connector(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"]},
|
||||
}
|
||||
|
||||
def search(self, query, min_confidence=None): # pylint: disable=arguments-differ
|
||||
"""overrides default search function with confidence ranking"""
|
||||
results = super().search(query)
|
||||
if min_confidence:
|
||||
# filter the search results after the fact
|
||||
return [r for r in results if r.confidence >= min_confidence]
|
||||
return results
|
||||
|
||||
def parse_search_data(self, data):
|
||||
return data.get("results")
|
||||
|
||||
def format_search_result(self, search_result):
|
||||
images = search_result.get("image")
|
||||
cover = f"{self.covers_url}/img/entities/{images[0]}" if images else None
|
||||
# a deeply messy translation of inventaire's scores
|
||||
confidence = float(search_result.get("_score", 0.1))
|
||||
confidence = 0.1 if confidence < 150 else 0.999
|
||||
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_search_data(self, data, min_confidence):
|
||||
for search_result in data.get("results", []):
|
||||
images = search_result.get("image")
|
||||
cover = f"{self.covers_url}/img/entities/{images[0]}" if images else None
|
||||
# a deeply messy translation of inventaire's scores
|
||||
confidence = float(search_result.get("_score", 0.1))
|
||||
confidence = 0.1 if confidence < 150 else 0.999
|
||||
if confidence < min_confidence:
|
||||
continue
|
||||
yield 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):
|
||||
"""got some daaaata"""
|
||||
results = data.get("entities")
|
||||
if not results:
|
||||
return []
|
||||
return list(results.values())
|
||||
|
||||
def format_isbn_search_result(self, search_result):
|
||||
"""totally different format than a regular search result"""
|
||||
title = search_result.get("claims", {}).get("wdt:P1476", [])
|
||||
if not title:
|
||||
return None
|
||||
return SearchResult(
|
||||
title=title[0],
|
||||
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=self.get_cover_url(search_result.get("image")),
|
||||
connector=self,
|
||||
)
|
||||
return
|
||||
for search_result in list(results.values()):
|
||||
title = search_result.get("claims", {}).get("wdt:P1476", [])
|
||||
if not title:
|
||||
continue
|
||||
yield SearchResult(
|
||||
title=title[0],
|
||||
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=self.get_cover_url(search_result.get("image")),
|
||||
connector=self,
|
||||
)
|
||||
|
||||
def is_work_data(self, data):
|
||||
return data.get("type") == "work"
|
||||
|
|
|
@ -152,39 +152,41 @@ class Connector(AbstractConnector):
|
|||
image_name = f"{cover_id}-{size}.jpg"
|
||||
return f"{self.covers_url}/b/id/{image_name}"
|
||||
|
||||
def parse_search_data(self, data):
|
||||
return data.get("docs")
|
||||
def parse_search_data(self, data, min_confidence):
|
||||
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):
|
||||
# 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
|
||||
return SearchResult(
|
||||
title=search_result.get("title"),
|
||||
key=key,
|
||||
author=", ".join(author),
|
||||
connector=self,
|
||||
year=search_result.get("first_publish_year"),
|
||||
cover=cover,
|
||||
)
|
||||
# OL doesn't provide confidence, but it does sort by an internal ranking, so
|
||||
# this confidence value is relative to the list position
|
||||
confidence = 1 / (idx + 1)
|
||||
|
||||
yield SearchResult(
|
||||
title=search_result.get("title"),
|
||||
key=key,
|
||||
author=", ".join(author),
|
||||
connector=self,
|
||||
year=search_result.get("first_publish_year"),
|
||||
cover=cover,
|
||||
confidence=confidence,
|
||||
)
|
||||
|
||||
def parse_isbn_search_data(self, data):
|
||||
return list(data.values())
|
||||
|
||||
def format_isbn_search_result(self, search_result):
|
||||
# build the remote id from the openlibrary key
|
||||
key = self.books_url + search_result["key"]
|
||||
authors = search_result.get("authors") or [{"name": "Unknown"}]
|
||||
author_names = [author.get("name") for author in authors]
|
||||
return SearchResult(
|
||||
title=search_result.get("title"),
|
||||
key=key,
|
||||
author=", ".join(author_names),
|
||||
connector=self,
|
||||
year=search_result.get("publish_date"),
|
||||
)
|
||||
for search_result in list(data.values()):
|
||||
# build the remote id from the openlibrary key
|
||||
key = self.books_url + search_result["key"]
|
||||
authors = search_result.get("authors") or [{"name": "Unknown"}]
|
||||
author_names = [author.get("name") for author in authors]
|
||||
yield SearchResult(
|
||||
title=search_result.get("title"),
|
||||
key=key,
|
||||
author=", ".join(author_names),
|
||||
connector=self,
|
||||
year=search_result.get("publish_date"),
|
||||
)
|
||||
|
||||
def load_edition_data(self, olkey):
|
||||
"""query openlibrary for editions of a work"""
|
||||
|
|
|
@ -4,6 +4,7 @@ from django import forms
|
|||
from bookwyrm import models
|
||||
from bookwyrm.models.fields import ClearableFileInputWithWarning
|
||||
from .custom_form import CustomForm
|
||||
from .widgets import ArrayWidget, SelectDateWidget, Select
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
|
@ -14,14 +15,6 @@ class CoverForm(CustomForm):
|
|||
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 Meta:
|
||||
model = models.Edition
|
||||
|
@ -56,16 +49,16 @@ class EditionForm(CustomForm):
|
|||
"publishers": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
|
||||
),
|
||||
"first_published_date": forms.SelectDateWidget(
|
||||
"first_published_date": SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_first_published_date"}
|
||||
),
|
||||
"published_date": forms.SelectDateWidget(
|
||||
"published_date": SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_published_date"}
|
||||
),
|
||||
"cover": ClearableFileInputWithWarning(
|
||||
attrs={"aria-describedby": "desc_cover"}
|
||||
),
|
||||
"physical_format": forms.Select(
|
||||
"physical_format": Select(
|
||||
attrs={"aria-describedby": "desc_physical_format"}
|
||||
),
|
||||
"physical_format_detail": forms.TextInput(
|
||||
|
@ -85,3 +78,27 @@ class EditionForm(CustomForm):
|
|||
),
|
||||
"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):
|
||||
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()
|
||||
start_date = cleaned_data.get("start_date")
|
||||
finish_date = cleaned_data.get("finish_date")
|
||||
|
@ -53,7 +53,12 @@ class ReadThroughForm(CustomForm):
|
|||
self.add_error(
|
||||
"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:
|
||||
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 """
|
||||
|
||||
from .importer import Importer
|
||||
from .calibre_import import CalibreImporter
|
||||
from .goodreads_import import GoodreadsImporter
|
||||
from .librarything_import import LibrarythingImporter
|
||||
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 """
|
||||
import re
|
||||
|
||||
from bookwyrm.models import Shelf
|
||||
|
||||
from . import Importer
|
||||
|
||||
|
||||
|
@ -21,7 +24,7 @@ class LibrarythingImporter(Importer):
|
|||
|
||||
def get_shelf(self, normalized_row):
|
||||
if normalized_row["date_finished"]:
|
||||
return "read"
|
||||
return Shelf.READ_FINISHED
|
||||
if normalized_row["date_started"]:
|
||||
return "reading"
|
||||
return "to-read"
|
||||
return Shelf.READING
|
||||
return Shelf.TO_READ
|
||||
|
|
|
@ -56,12 +56,17 @@ class Command(BaseCommand):
|
|||
self.stdout.write(" OK 🖼")
|
||||
|
||||
# Books
|
||||
books = models.Book.objects.select_subclasses().filter()
|
||||
self.stdout.write(
|
||||
" → Book preview images ({}): ".format(len(books)), ending=""
|
||||
book_ids = (
|
||||
models.Book.objects.select_subclasses()
|
||||
.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(" OK 🖼")
|
||||
|
||||
|
|
|
@ -89,7 +89,7 @@ def init_connectors():
|
|||
covers_url="https://inventaire.io",
|
||||
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",
|
||||
priority=3,
|
||||
priority=1,
|
||||
)
|
||||
|
||||
models.Connector.objects.create(
|
||||
|
@ -101,20 +101,10 @@ def init_connectors():
|
|||
covers_url="https://covers.openlibrary.org",
|
||||
search_url="https://openlibrary.org/search?q=",
|
||||
isbn_search_url="https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:",
|
||||
priority=3,
|
||||
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():
|
||||
"""info about the instance"""
|
||||
models.SiteSettings.objects.create(
|
||||
|
@ -163,7 +153,6 @@ class Command(BaseCommand):
|
|||
"group",
|
||||
"permission",
|
||||
"connector",
|
||||
"federatedserver",
|
||||
"settings",
|
||||
"linkdomain",
|
||||
]
|
||||
|
@ -176,8 +165,6 @@ class Command(BaseCommand):
|
|||
init_permissions()
|
||||
if not limit or limit == "connector":
|
||||
init_connectors()
|
||||
if not limit or limit == "federatedserver":
|
||||
init_federated_servers()
|
||||
if not limit or limit == "settings":
|
||||
init_settings()
|
||||
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.http import Http404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.text import slugify
|
||||
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from .fields import RemoteIdField
|
||||
|
@ -35,10 +36,11 @@ class BookWyrmModel(models.Model):
|
|||
remote_id = RemoteIdField(null=True, activitypub_field="id")
|
||||
|
||||
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}"
|
||||
if hasattr(self, "user"):
|
||||
base_path = f"{base_path}{self.user.local_path}"
|
||||
|
||||
model_name = type(self).__name__.lower()
|
||||
return f"{base_path}/{model_name}/{self.id}"
|
||||
|
||||
|
@ -49,8 +51,20 @@ class BookWyrmModel(models.Model):
|
|||
|
||||
@property
|
||||
def local_path(self):
|
||||
"""how to link to this object in the local app"""
|
||||
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
|
||||
"""how to link to this object in the local app, with a slug"""
|
||||
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):
|
||||
"""is a user authorized to view an object?"""
|
||||
|
|
|
@ -176,8 +176,8 @@ class Book(BookDataModel):
|
|||
"""properties of this edition, as a string"""
|
||||
items = [
|
||||
self.physical_format if hasattr(self, "physical_format") else None,
|
||||
self.languages[0] + " language"
|
||||
if self.languages and self.languages[0] != "English"
|
||||
f"{self.languages[0]} language"
|
||||
if self.languages and self.languages[0] and self.languages[0] != "English"
|
||||
else None,
|
||||
str(self.published_date.year) if self.published_date else None,
|
||||
", ".join(self.publishers) if hasattr(self, "publishers") else None,
|
||||
|
|
|
@ -125,7 +125,7 @@ class ActivitypubFieldMixin:
|
|||
"""model_field_name to activitypubFieldName"""
|
||||
if self.activitypub_field:
|
||||
return self.activitypub_field
|
||||
name = self.name.split(".")[-1]
|
||||
name = self.name.rsplit(".", maxsplit=1)[-1]
|
||||
components = name.split("_")
|
||||
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
|
||||
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):
|
||||
"""helper function for assinging a value to the field"""
|
||||
value = getattr(data, self.get_activitypub_field())
|
||||
|
|
|
@ -175,9 +175,15 @@ class ImportItem(models.Model):
|
|||
def date_added(self):
|
||||
"""when the book was added to this dataset"""
|
||||
if self.normalized_data.get("date_added"):
|
||||
return timezone.make_aware(
|
||||
dateutil.parser.parse(self.normalized_data.get("date_added"))
|
||||
parsed_date_added = dateutil.parser.parse(
|
||||
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
|
||||
|
||||
@property
|
||||
|
|
|
@ -27,6 +27,7 @@ class ReadThrough(BookWyrmModel):
|
|||
)
|
||||
start_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)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
@ -34,7 +35,7 @@ class ReadThrough(BookWyrmModel):
|
|||
cache.delete(f"latest_read_through-{self.user.id}-{self.book.id}")
|
||||
self.user.update_active_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
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -39,15 +39,14 @@ class UserRelationship(BookWyrmModel):
|
|||
|
||||
def save(self, *args, **kwargs):
|
||||
"""clear the template cache"""
|
||||
# invalidate the template cache
|
||||
cache.delete_many(
|
||||
[
|
||||
f"relationship-{self.user_subject.id}-{self.user_object.id}",
|
||||
f"relationship-{self.user_object.id}-{self.user_subject.id}",
|
||||
]
|
||||
)
|
||||
clear_cache(self.user_subject, self.user_object)
|
||||
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:
|
||||
"""relationships should be unique"""
|
||||
|
||||
|
@ -90,7 +89,9 @@ class UserFollows(ActivityMixin, UserRelationship):
|
|||
user_object=self.user_subject,
|
||||
)
|
||||
).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
|
||||
# are handled by the UserFollowRequest model
|
||||
super().save(*args, broadcast=False, **kwargs)
|
||||
|
@ -98,11 +99,12 @@ class UserFollows(ActivityMixin, UserRelationship):
|
|||
@classmethod
|
||||
def from_request(cls, follow_request):
|
||||
"""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_object=follow_request.user_object,
|
||||
remote_id=follow_request.remote_id,
|
||||
)
|
||||
return obj
|
||||
|
||||
|
||||
class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||
|
@ -133,7 +135,9 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
|||
user_object=self.user_subject,
|
||||
)
|
||||
).exists():
|
||||
raise IntegrityError()
|
||||
raise IntegrityError(
|
||||
"Attempting to follow blocked user", self.user_subject, self.user_object
|
||||
)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if broadcast and self.user_subject.local and not self.user_object.local:
|
||||
|
@ -174,7 +178,8 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
|||
|
||||
with transaction.atomic():
|
||||
UserFollows.from_request(self)
|
||||
self.delete()
|
||||
if self.id:
|
||||
self.delete()
|
||||
|
||||
def reject(self):
|
||||
"""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_object, user_object=self.user_subject)
|
||||
).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 bookwyrm import activitypub
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
||||
from .base_model import BookWyrmModel
|
||||
from . import fields
|
||||
|
@ -17,8 +18,9 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
|||
TO_READ = "to-read"
|
||||
READING = "reading"
|
||||
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)
|
||||
identifier = models.CharField(max_length=100)
|
||||
|
@ -65,6 +67,11 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
|||
identifier = self.identifier or self.get_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):
|
||||
"""don't let anyone delete a default shelf"""
|
||||
super().raise_not_deletable(viewer)
|
||||
|
|
|
@ -116,11 +116,8 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
|||
def ignore_activity(cls, activity): # pylint: disable=too-many-return-statements
|
||||
"""keep notes if they are replies to existing statuses"""
|
||||
if activity.type == "Announce":
|
||||
try:
|
||||
boosted = activitypub.resolve_remote_id(
|
||||
activity.object, get_activity=True
|
||||
)
|
||||
except activitypub.ActivitySerializerError:
|
||||
boosted = activitypub.resolve_remote_id(activity.object, get_activity=True)
|
||||
if not boosted:
|
||||
# if we can't load the status, definitely ignore it
|
||||
return True
|
||||
# keep the boost if we would keep the status
|
||||
|
@ -265,7 +262,7 @@ class GeneratedNote(Status):
|
|||
|
||||
|
||||
ReadingStatusChoices = models.TextChoices(
|
||||
"ReadingStatusChoices", ["to-read", "reading", "read"]
|
||||
"ReadingStatusChoices", ["to-read", "reading", "read", "stopped-reading"]
|
||||
)
|
||||
|
||||
|
||||
|
@ -306,10 +303,17 @@ class Comment(BookStatus):
|
|||
@property
|
||||
def pure_content(self):
|
||||
"""indicate the book in question for mastodon (or w/e) users"""
|
||||
return (
|
||||
f'{self.content}<p>(comment on <a href="{self.book.remote_id}">'
|
||||
f'"{self.book.title}"</a>)</p>'
|
||||
)
|
||||
if self.progress_mode == "PG" and self.progress and (self.progress > 0):
|
||||
return_value = (
|
||||
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
|
||||
|
||||
|
@ -335,10 +339,17 @@ class Quotation(BookStatus):
|
|||
"""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>', quote)
|
||||
return (
|
||||
f'{quote} <p>-- <a href="{self.book.remote_id}">'
|
||||
f'"{self.book.title}"</a></p>{self.content}'
|
||||
)
|
||||
if self.position_mode == "PG" and self.position and (self.position > 0):
|
||||
return_value = (
|
||||
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
|
||||
|
||||
|
@ -377,7 +388,7 @@ class Review(BookStatus):
|
|||
def save(self, *args, **kwargs):
|
||||
"""clear rating caches"""
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -374,6 +374,10 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
"name": "Read",
|
||||
"identifier": "read",
|
||||
},
|
||||
{
|
||||
"name": "Stopped Reading",
|
||||
"identifier": "stopped-reading",
|
||||
},
|
||||
]
|
||||
|
||||
for shelf in shelves:
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
env = Env()
|
||||
env.read_env()
|
||||
DOMAIN = env("DOMAIN")
|
||||
VERSION = "0.3.4"
|
||||
VERSION = "0.4.0"
|
||||
|
||||
RELEASE_API = env(
|
||||
"RELEASE_API",
|
||||
|
@ -21,7 +21,7 @@ RELEASE_API = env(
|
|||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||
|
||||
JS_CACHE = "bc93172a"
|
||||
JS_CACHE = "e678183b"
|
||||
|
||||
# email
|
||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||
|
@ -212,7 +212,7 @@ STREAMS = [
|
|||
|
||||
# Search configuration
|
||||
# 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
|
||||
QUERY_TIMEOUT = int(env("QUERY_TIMEOUT", 5))
|
||||
|
||||
|
@ -284,11 +284,13 @@ LANGUAGES = [
|
|||
("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)")),
|
||||
|
|
|
@ -34,6 +34,18 @@ body {
|
|||
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 {
|
||||
border: none;
|
||||
margin: 0;
|
||||
|
@ -125,13 +137,6 @@ button:focus-visible .button-invisible-overlay {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
/** Tooltips
|
||||
******************************************************************************/
|
||||
|
||||
.tooltip {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/** States
|
||||
******************************************************************************/
|
||||
|
||||
|
|
|
@ -114,3 +114,17 @@ details[open] summary .details-close {
|
|||
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 {
|
||||
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;
|
||||
$info: #1f4666;
|
||||
$success: #246447;
|
||||
$success-light: #0d2f1e;
|
||||
$warning: #8b6c15;
|
||||
$warning-light: #372e13;
|
||||
$danger: #872538;
|
||||
$danger-light: #481922;
|
||||
$light: #393939;
|
||||
|
@ -26,6 +28,8 @@ $background-body: rgb(24, 27, 28);
|
|||
$background-secondary: rgb(28, 30, 32);
|
||||
$background-tertiary: rgb(32, 34, 36);
|
||||
$modal-background-background-color: rgba($black, 0.8);
|
||||
$scrollbar-track: $background-secondary;
|
||||
$scrollbar-thumb: $light;
|
||||
|
||||
/* highlight colors */
|
||||
$primary-highlight: $primary;
|
||||
|
@ -49,6 +53,7 @@ $link-hover: $white-bis;
|
|||
$link-hover-border: #51595d;
|
||||
$link-focus: $white-bis;
|
||||
$link-active: $white-bis;
|
||||
$link-light: #0d1c26;
|
||||
|
||||
/* bulma overrides */
|
||||
$background: $background-secondary;
|
||||
|
@ -79,5 +84,13 @@ $progress-value-background-color: $border-light;
|
|||
$family-primary: $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 "../vendor/icons.css";
|
||||
|
|
|
@ -19,6 +19,8 @@ $scheme-main: $white-bis;
|
|||
$background-body: $white;
|
||||
$background-secondary: $white-ter;
|
||||
$background-tertiary: $white-bis;
|
||||
$scrollbar-track: $background-secondary;
|
||||
$scrollbar-thumb: $grey-lighter;
|
||||
|
||||
/* highlight colors */
|
||||
$primary-highlight: $primary-light;
|
||||
|
@ -55,5 +57,14 @@ $invisible-overlay-background-color: rgba($scheme-invert, 0.66);
|
|||
$family-primary: $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 "../vendor/icons.css";
|
||||
|
|
|
@ -203,6 +203,8 @@ let StatusCache = new (class {
|
|||
.forEach((item) => (item.disabled = false));
|
||||
|
||||
next_identifier = next_identifier == "complete" ? "read" : next_identifier;
|
||||
next_identifier =
|
||||
next_identifier == "stopped-reading-complete" ? "stopped-reading" : next_identifier;
|
||||
|
||||
// Disable the current state
|
||||
button.querySelector(
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
<p>
|
||||
{% url "conduct" as coc_path %}
|
||||
{% 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 %}
|
||||
</p>
|
||||
</header>
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="column">
|
||||
<div class="column is-clipped">
|
||||
{% block about_content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
</div>
|
||||
{% 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 %}
|
||||
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
||||
|
||||
|
|
|
@ -208,9 +208,17 @@
|
|||
{% endif %}
|
||||
|
||||
|
||||
{% if book.parent_work.editions.count > 1 %}
|
||||
<p>{% blocktrans with path=book.parent_work.local_path count=book.parent_work.editions.count %}<a href="{{ path }}/editions">{{ count }} editions</a>{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
{% with work=book.parent_work %}
|
||||
<p>
|
||||
<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>
|
||||
|
||||
{# 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 %}
|
||||
<nav class="tabs">
|
||||
<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 %}>
|
||||
<a href="{{ tab_url }}#reviews">{% trans "Reviews" %} ({{ review_count }})</a>
|
||||
</li>
|
||||
|
|
|
@ -3,18 +3,24 @@
|
|||
{% load humanize %}
|
||||
{% 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 %}
|
||||
<header class="block">
|
||||
<h1 class="title level-left">
|
||||
{% if book %}
|
||||
{% if book.title %}
|
||||
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "Add Book" %}
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if book %}
|
||||
{% if book.created_date %}
|
||||
<dl>
|
||||
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Added:" %}</dt>
|
||||
<dd class="ml-2">{{ book.created_date | naturaltime }}</dd>
|
||||
|
@ -33,12 +39,20 @@
|
|||
|
||||
<form
|
||||
class="block"
|
||||
{% if book %}
|
||||
{% if book.id %}
|
||||
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 %}
|
||||
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 %}
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
|
@ -97,7 +111,7 @@
|
|||
<input type="radio" name="parent_work" value="{{ match.parent_work.id }}"> {{ match.parent_work.title }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
<label>
|
||||
<label class="label mt-2">
|
||||
<input type="radio" name="parent_work" value="0" required> {% trans "This is a new work" %}
|
||||
</label>
|
||||
</fieldset>
|
||||
|
@ -119,7 +133,7 @@
|
|||
{% if not confirm_mode %}
|
||||
<div class="block">
|
||||
<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>
|
||||
{% else %}
|
||||
<a href="/" class="button" data-back>
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
{% csrf_token %}
|
||||
|
||||
<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="column is-half">
|
||||
<section class="block">
|
||||
|
@ -153,8 +155,7 @@
|
|||
<label class="label" for="id_first_published_date">
|
||||
{% trans "First published date:" %}
|
||||
</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" %}
|
||||
</div>
|
||||
|
||||
|
@ -162,7 +163,7 @@
|
|||
<label class="label" for="id_published_date">
|
||||
{% trans "Published date:" %}
|
||||
</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" %}
|
||||
</div>
|
||||
|
@ -175,6 +176,8 @@
|
|||
</h2>
|
||||
<div class="box">
|
||||
{% 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>
|
||||
{% for author in book.authors.all %}
|
||||
<div class="is-flex is-justify-content-space-between">
|
||||
|
@ -255,9 +258,7 @@
|
|||
<label class="label" for="id_physical_format">
|
||||
{% trans "Format:" %}
|
||||
</label>
|
||||
<div class="select">
|
||||
{{ form.physical_format }}
|
||||
</div>
|
||||
{{ form.physical_format }}
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.physical_format.errors id="desc_physical_format" %}
|
||||
</div>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
<div class="column my-3-mobile ml-3-tablet mr-auto">
|
||||
<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 }}
|
||||
</a>
|
||||
</h2>
|
||||
|
@ -46,7 +46,36 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="block">
|
||||
{% include 'snippets/pagination.html' with page=editions path=request.path %}
|
||||
</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 %}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<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">
|
||||
<a href="#" aria-current="page">
|
||||
{% 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 class="block">
|
||||
{% trans "Can't find your code?" as button_text %}
|
||||
{% include "snippets/toggle/open_button.html" with text=button_text controls_text="resend_form" focus="resend_form_header" %}
|
||||
{% include "confirm_email/resend_form.html" with controls_text="resend_form" %}
|
||||
<form name="fallback" method="GET" action="{% url 'resend-link' %}" autocomplete="off">
|
||||
<button
|
||||
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>
|
||||
</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 %}
|
||||
<p>
|
||||
<a href="https://joinbookwyrm.com/">
|
||||
{% trans "Join Bookwyrm" %}
|
||||
{% trans "Join BookWyrm" %}
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
|
|
@ -5,7 +5,19 @@
|
|||
<section class="block">
|
||||
<h2 class="title is-4">{% trans "Your Books" %}</h2>
|
||||
{% 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 %}
|
||||
{% with active_book=request.GET.book %}
|
||||
<div class="tab-group">
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
|
||||
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
|
||||
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
|
||||
{% elif shelf.identifier == 'stopped-reading' %}{% trans "Stopped Reading" %}
|
||||
{% else %}{{ shelf.name }}{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
{% for membership in group.memberships.all %}
|
||||
{% with member=membership.user %}
|
||||
<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 %}
|
||||
<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>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<div class="column is-flex is-flex-grow-0">
|
||||
{% for user in suggested_users %}
|
||||
<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 %}
|
||||
<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>
|
||||
|
|
|
@ -14,28 +14,35 @@
|
|||
<div class="column is-half">
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-pulled-left" for="source">
|
||||
<label class="label" for="source">
|
||||
{% trans "Data source:" %}
|
||||
</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 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">
|
||||
<label class="label" for="id_csv_file">{% trans "Data file:" %}</label>
|
||||
{{ import_form.csv_file }}
|
||||
|
@ -63,7 +70,7 @@
|
|||
<div class="content block">
|
||||
<h2 class="title">{% trans "Recent Imports" %}</h2>
|
||||
{% if not jobs %}
|
||||
<p>{% trans "No recent imports" %}</p>
|
||||
<p><em>{% trans "No recent imports" %}</em></p>
|
||||
{% endif %}
|
||||
<ul>
|
||||
{% 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">
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="navbar-item mt-3 py-0 has-dropdown is-hoverable">
|
||||
<a
|
||||
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 class="navbar-item mt-3 py-0">
|
||||
{% include 'user_menu.html' %}
|
||||
</div>
|
||||
<div class="navbar-item mt-3 py-0">
|
||||
<a href="{% url 'notifications' %}" class="tags has-addons">
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<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">
|
||||
<a href="#" aria-current="page">
|
||||
{% trans "Curate" %}
|
||||
|
|
|
@ -180,7 +180,7 @@
|
|||
<h2 class="title is-5">
|
||||
{% trans "Sort List" %}
|
||||
</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">
|
||||
<label class="label" for="id_sort_by">{% trans "Sort By" %}</label>
|
||||
<div class="select is-fullwidth">
|
||||
|
@ -207,7 +207,7 @@
|
|||
{% trans "Suggest Books" %}
|
||||
{% endif %}
|
||||
</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="control">
|
||||
<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>
|
||||
{% 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 %}
|
||||
</form>
|
||||
{% if not suggested_books %}
|
||||
|
|
|
@ -47,12 +47,12 @@
|
|||
|
||||
|
||||
{% 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="column is-clipped">
|
||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||
</div>
|
||||
<div class="column is-narrow has-grey-dark">
|
||||
<div class="column is-narrow has-text-muted">
|
||||
{{ related_status.published_date|timesince }}
|
||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||
</div>
|
||||
|
|
|
@ -47,12 +47,12 @@
|
|||
|
||||
|
||||
{% 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="column is-clipped">
|
||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||
</div>
|
||||
<div class="column is-narrow has-grey-dark">
|
||||
<div class="column is-narrow has-text-muted">
|
||||
{{ related_status.published_date|timesince }}
|
||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% load notification_page_tags %}
|
||||
{% related_status notification as related_status %}
|
||||
<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">
|
||||
<a class="icon" href="{% block primary_link %}{% endblock %}">
|
||||
{% block icon %}{% endblock %}
|
||||
|
|
|
@ -48,12 +48,12 @@
|
|||
|
||||
|
||||
{% 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="column is-clipped">
|
||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||
</div>
|
||||
<div class="column is-narrow has-text-black">
|
||||
<div class="column is-narrow has-text-default">
|
||||
{{ related_status.published_date|timesince }}
|
||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||
</div>
|
||||
|
|
|
@ -51,12 +51,12 @@
|
|||
|
||||
|
||||
{% 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="column is-clipped">
|
||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||
</div>
|
||||
<div class="column is-narrow has-text-black">
|
||||
<div class="column is-narrow has-text-default">
|
||||
{{ related_status.published_date|timesince }}
|
||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||
</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>
|
||||
</li>
|
||||
</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>
|
||||
<ul class="menu-list">
|
||||
<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>
|
||||
{% include "snippets/progress_field.html" with id=field_id %}
|
||||
{% endif %}
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_finish_date_{{ readthrough.id }}">
|
||||
{% trans "Finished reading" %}
|
||||
|
|
|
@ -8,10 +8,12 @@
|
|||
<div class="column">
|
||||
{% trans "Progress Updates:" %}
|
||||
<ul>
|
||||
{% if readthrough.finish_date or readthrough.progress %}
|
||||
{% if readthrough.finish_date or readthrough.stopped_date or readthrough.progress %}
|
||||
<li>
|
||||
{% if readthrough.finish_date %}
|
||||
{{ readthrough.finish_date | localtime | naturalday }}: {% trans "finished" %}
|
||||
{% elif readthrough.stopped_date %}
|
||||
{{ readthrough.stopped_date | localtime | naturalday }}: {% trans "stopped" %}
|
||||
{% else %}
|
||||
|
||||
{% if readthrough.progress_mode == 'PG' %}
|
||||
|
|
|
@ -17,7 +17,14 @@
|
|||
{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% block modal-body %}
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
{% if result_set.results %}
|
||||
<section class="mb-5">
|
||||
{% if not result_set.connector.local %}
|
||||
<details class="details-panel box" {% if forloop.first %}open{% endif %}>
|
||||
<details class="details-panel box" open>
|
||||
{% endif %}
|
||||
{% if not result_set.connector.local %}
|
||||
<summary class="is-flex is-align-items-center is-flex-wrap-wrap is-gap-2">
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
{% block panel %}
|
||||
<div class="block table-container">
|
||||
<table class="table is-striped">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tr>
|
||||
<th>
|
||||
{% url 'settings-announcements' as url %}
|
||||
|
|
|
@ -154,7 +154,7 @@
|
|||
</summary>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table is-striped">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tr>
|
||||
<th>
|
||||
<label for="id_string_match">{% trans "String match" %}</label>
|
||||
|
|
|
@ -10,26 +10,26 @@
|
|||
{% block panel %}
|
||||
|
||||
<div class="columns block has-text-centered is-mobile is-multiline">
|
||||
<div class="column is-3-desktop is-6-mobile">
|
||||
<div class="notification">
|
||||
<div class="column is-3-desktop is-6-mobile is-flex">
|
||||
<div class="notification is-flex-grow-1">
|
||||
<h3>{% trans "Total users" %}</h3>
|
||||
<p class="title is-5">{{ users|intcomma }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3-desktop is-6-mobile">
|
||||
<div class="notification">
|
||||
<div class="column is-3-desktop is-6-mobil is-flexe">
|
||||
<div class="notification is-flex-grow-1">
|
||||
<h3>{% trans "Active this month" %}</h3>
|
||||
<p class="title is-5">{{ active_users|intcomma }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3-desktop is-6-mobile">
|
||||
<div class="notification">
|
||||
<div class="column is-3-desktop is-6-mobile is-flex">
|
||||
<div class="notification is-flex-grow-1">
|
||||
<h3>{% trans "Statuses" %}</h3>
|
||||
<p class="title is-5">{{ statuses|intcomma }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3-desktop is-6-mobile">
|
||||
<div class="notification">
|
||||
<div class="column is-3-desktop is-6-mobile is-flex">
|
||||
<div class="notification is-flex-grow-1">
|
||||
<h3>{% trans "Works" %}</h3>
|
||||
<p class="title is-5">{{ works|intcomma }}</p>
|
||||
</div>
|
||||
|
@ -38,8 +38,8 @@
|
|||
|
||||
<div class="columns block is-multiline">
|
||||
{% if reports %}
|
||||
<div class="column">
|
||||
<a href="{% url 'settings-reports' %}" class="notification is-warning is-block">
|
||||
<div class="column is-flex">
|
||||
<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 %}
|
||||
{{ display_count }} open report
|
||||
{% plural %}
|
||||
|
@ -50,8 +50,8 @@
|
|||
{% endif %}
|
||||
|
||||
{% if pending_domains %}
|
||||
<div class="column">
|
||||
<a href="{% url 'settings-link-domain' %}" class="notification is-primary is-block">
|
||||
<div class="column is-flex">
|
||||
<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 %}
|
||||
{{ display_count }} domain needs review
|
||||
{% plural %}
|
||||
|
@ -62,8 +62,8 @@
|
|||
{% endif %}
|
||||
|
||||
{% if not site.allow_registration and site.allow_invite_requests and invite_requests %}
|
||||
<div class="column">
|
||||
<a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success">
|
||||
<div class="column is-flex">
|
||||
<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 %}
|
||||
{{ display_count }} invite request
|
||||
{% plural %}
|
||||
|
@ -74,8 +74,8 @@
|
|||
{% endif %}
|
||||
|
||||
{% if current_version %}
|
||||
<div class="column">
|
||||
<a href="https://docs.joinbookwyrm.com/updating-your-instance.html" class="notification is-block is-warning" target="_blank">
|
||||
<div class="column is-flex">
|
||||
<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 %}
|
||||
An update is available! You're running v{{ current }} and the latest release is {{ available }}.
|
||||
{% 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 %}
|
||||
|
||||
{% block panel %}
|
||||
|
||||
{% include 'settings/federation/instance_filters.html' %}
|
||||
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
{% url 'settings-federation' status='federated' as url %}
|
||||
|
@ -25,7 +28,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<table class="table is-striped">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tr>
|
||||
{% url 'settings-federation' as url %}
|
||||
<th>
|
||||
|
@ -36,6 +39,10 @@
|
|||
{% trans "Date added" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Last updated" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="updated_date" sort=sort text=text %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Software" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="application_type" sort=sort text=text %}
|
||||
|
@ -43,12 +50,12 @@
|
|||
<th>
|
||||
{% trans "Users" %}
|
||||
</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
</tr>
|
||||
{% for server in servers %}
|
||||
<tr>
|
||||
<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>
|
||||
{% if server.application_type %}
|
||||
{{ server.application_type }}
|
||||
|
@ -56,7 +63,6 @@
|
|||
{% endif %}
|
||||
</td>
|
||||
<td>{{ server.user_set.count }}</td>
|
||||
<td>{{ server.get_status_display }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% 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">
|
||||
<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>
|
||||
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<div class="column">
|
||||
<div class="column is-clipped">
|
||||
{% block panel %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -44,5 +44,6 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% include 'snippets/pagination.html' with page=reports path=request.path %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -139,6 +139,13 @@
|
|||
{% trans "Allow registration" %}
|
||||
</label>
|
||||
</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">
|
||||
<label class="label" for="id_allow_invite_requests">
|
||||
{{ site_form.allow_invite_requests }}
|
||||
|
@ -157,13 +164,6 @@
|
|||
{{ site_form.invite_question_text }}
|
||||
</label>
|
||||
</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">
|
||||
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
|
||||
{{ site_form.registration_closed_text }}
|
||||
|
@ -171,7 +171,7 @@
|
|||
<div class="field">
|
||||
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
|
||||
{{ site_form.invite_request_text }}
|
||||
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=site_form.invite_request_text.errors id="desc_invite_request_text" %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
<section class="block content">
|
||||
<h2 class="title is-4">{% trans "Available Themes" %}</h2>
|
||||
<div class="table-container">
|
||||
<table class="table is-striped">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tr>
|
||||
<th>
|
||||
{% trans "Theme name" %}
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
|
||||
<div class="table-container block">
|
||||
<table class="table is-striped">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tr>
|
||||
{% url 'settings-users' as url %}
|
||||
<th>
|
||||
|
@ -61,10 +61,25 @@
|
|||
</tr>
|
||||
{% for user in users %}
|
||||
<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.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" %}
|
||||
<td>
|
||||
{% if user.federated_server %}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<div class="column is-flex is-flex-direction-column">
|
||||
<h4 class="title is-4">{% trans "Profile" %}</h4>
|
||||
<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 %}
|
||||
<div class="box content has-background-secondary is-shadowless">
|
||||
{{ user.summary|to_markdown|safe }}
|
||||
|
@ -14,6 +14,10 @@
|
|||
{% endif %}
|
||||
|
||||
<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 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>
|
||||
<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>
|
||||
<dd>{{ user.last_active_date }}</dd>
|
||||
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
|
||||
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
|
||||
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
|
||||
{% elif shelf.identifier == 'stopped-reading' %}{% trans "Stopped Reading" %}
|
||||
{% else %}{{ shelf.name }}{% endif %}
|
||||
<span class="subtitle">
|
||||
{% include 'snippets/privacy-icons.html' with item=shelf %}
|
||||
|
@ -150,7 +151,7 @@
|
|||
{% 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 "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 %}
|
||||
<th>{% trans "Rating" as text %}{% include 'snippets/table-sort-header.html' with field="rating" sort=sort text=text %}</th>
|
||||
{% endif %}
|
||||
|
@ -180,7 +181,7 @@
|
|||
<td data-title="{% trans "Started" %}">
|
||||
{{ book.start_date|naturalday|default_if_none:""}}
|
||||
</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:""}}
|
||||
</td>
|
||||
{% 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 %}
|
||||
{% endif %}
|
||||
</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 %}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<form name="reading-progress-{{ uuid }}" action="{% url 'reading-status-update' book.id %}" method="POST" class="submit-status">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
||||
<input type="hidden" name="start_date" value="{{ readthrough.start_date|date:'Y-m-d' }}">
|
||||
{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
{% 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' %}
|
||||
|
||||
{% trans "Want to read" as button_text %}
|
||||
|
@ -99,5 +106,8 @@
|
|||
{% 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="" %}
|
||||
|
||||
{% 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 %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -29,6 +29,9 @@
|
|||
{% 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="" %}
|
||||
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
<li role="menuitem" class="dropdown-item p-0">
|
||||
<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-next="{{ shelf.identifier|next_shelf }}"
|
||||
>
|
||||
|
@ -26,6 +26,13 @@
|
|||
{% join "finish_reading" button_uuid as modal_id %}
|
||||
{% 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' %}
|
||||
|
||||
{% trans "Want to read" as button_text %}
|
||||
|
|
|
@ -13,6 +13,15 @@
|
|||
</button>
|
||||
</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 %}
|
||||
<div
|
||||
class="{% if next_shelf_identifier != shelf.identifier %}is-hidden{% endif %}"
|
||||
|
@ -33,6 +42,14 @@
|
|||
{% join "finish_reading" button_uuid as modal_id %}
|
||||
{% 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' %}
|
||||
|
||||
{% trans "Want to read" as button_text %}
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
{% endwith %}
|
||||
{% 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' %}
|
||||
<header class="mb-2">
|
||||
<h3
|
||||
|
@ -112,6 +112,9 @@
|
|||
{% with full=status.content|safe no_trim=status.content_warning itemprop="reviewBody" %}
|
||||
{% include 'snippets/trimmed_text.html' %}
|
||||
{% endwith %}
|
||||
{% if status.progress %}
|
||||
<div class="is-small is-italic has-text-right mr-3">{% trans "page" %} {{ status.progress }}</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if status.attachments.exists %}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
{% spaceless %}
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
{% load status_display %}
|
||||
|
||||
{% load_book status as book %}
|
||||
{% if book.authors.exists %}
|
||||
|
||||
{% with author=book.authors.first %}
|
||||
{% blocktrans trimmed with book_path=book.local_path book=book|book_title author_name=author.name author_path=author.local_path %}
|
||||
stopped reading <a href="{{ book_path }}">{{ book }}</a> by <a href="{{ author_path }}">{{ author_name }}</a>
|
||||
{% endblocktrans %}
|
||||
{% endwith %}
|
||||
|
||||
{% else %}
|
||||
|
||||
{% blocktrans trimmed with book_path=book.local_path book=book|book_title %}
|
||||
stopped reading <a href="{{ book_path }}">{{ book }}</a>
|
||||
{% endblocktrans %}
|
||||
|
||||
{% endif %}
|
||||
{% endspaceless %}
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<div class="card-footer-item">
|
||||
{% trans "Reply" as button_text %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with controls_text="show_comment" controls_uid=status.id text=button_text icon_with_text="comment" class="is-small is-light is-transparent toggle-button" focus="id_content_reply" %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with controls_text="show_comment" controls_uid=status.id text=button_text icon_with_text="comment" class="is-small is-light toggle-button" focus="id_content_reply" %}
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
{% include 'snippets/boost_button.html' with status=status %}
|
||||
|
@ -42,7 +42,7 @@
|
|||
</div>
|
||||
{% if not moderation_mode %}
|
||||
<div class="card-footer-item">
|
||||
{% include 'snippets/status/status_options.html' with class="is-small is-light is-transparent" right=True %}
|
||||
{% include 'snippets/status/status_options.html' with class="is-small is-light" right=True %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% for user in suggested_users %}
|
||||
<div class="column is-flex is-flex-grow-0">
|
||||
<div class="box has-text-centered is-shadowless has-background-tertiary m-0">
|
||||
<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 %}
|
||||
<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>
|
||||
|
|
|
@ -33,8 +33,9 @@
|
|||
{% if shelf.name == 'To Read' %}{% trans "To Read" %}
|
||||
{% elif shelf.name == 'Currently Reading' %}{% trans "Currently Reading" %}
|
||||
{% elif shelf.name == 'Read' %}{% trans "Read" %}
|
||||
{% elif shelf.name == 'Stopped Reading' %}{% trans "Stopped Reading" %}
|
||||
{% else %}{{ shelf.name }}{% endif %}
|
||||
{% if shelf.size > 3 %}<small>(<a href="{{ shelf.local_path }}">{% blocktrans with size=shelf.size %}View all {{ size }}{% endblocktrans %}</a>)</small>{% endif %}
|
||||
{% if shelf.size > 4 %}<small>(<a href="{{ shelf.local_path }}">{% blocktrans with size=shelf.size %}View all {{ size }}{% endblocktrans %}</a>)</small>{% endif %}
|
||||
</h3>
|
||||
<div class="is-mobile field is-grouped">
|
||||
{% for book in shelf.books %}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue