Merge branch 'main' into suggestions-redis

This commit is contained in:
Mouse Reeve 2021-05-20 20:25:15 -07:00 committed by GitHub
commit 07fc4a2efc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
200 changed files with 11270 additions and 6135 deletions

View file

@ -7,6 +7,9 @@ DEBUG=true
DOMAIN=your.domain.here
#EMAIL=your@email.here
# Used for deciding which editions to prefer
DEFAULT_LANGUAGE="English"
## Leave unset to allow all hosts
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
@ -18,6 +21,7 @@ BOOKWYRM_DATABASE_BACKEND=postgres
MEDIA_ROOT=images/
POSTGRES_PORT=5432
POSTGRES_PASSWORD=fedireads
POSTGRES_USER=fedireads
POSTGRES_DB=fedireads

View file

@ -7,6 +7,9 @@ DEBUG=false
DOMAIN=your.domain.here
EMAIL=your@email.here
# Used for deciding which editions to prefer
DEFAULT_LANGUAGE="English"
## Leave unset to allow all hosts
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
@ -18,6 +21,7 @@ BOOKWYRM_DATABASE_BACKEND=postgres
MEDIA_ROOT=images/
POSTGRES_PORT=5432
POSTGRES_PASSWORD=securedbpassword123
POSTGRES_USER=fedireads
POSTGRES_DB=fedireads

View file

@ -8,6 +8,4 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: psf/black@stable
with:
args: ". --check -l 80 -S"
- uses: psf/black@21.4b2

View file

@ -9,10 +9,11 @@ Permission is hereby granted, free of charge, to any person or organization (the
1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software.
2. The User is one of the following:
a. An individual person, laboring for themselves
b. A non-profit organization
c. An educational institution
d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor
1. An individual person, laboring for themselves
2. A non-profit organization
3. An educational institution
4. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor
3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote.

View file

@ -7,11 +7,22 @@ from .image import Document
@dataclass(init=False)
class Book(ActivityObject):
class BookData(ActivityObject):
"""shared fields for all book data and authors"""
openlibraryKey: str = None
inventaireId: str = None
librarythingKey: str = None
goodreadsKey: str = None
bnfId: str = None
lastEditedBy: str = None
@dataclass(init=False)
class Book(BookData):
"""serializes an edition or work, abstract"""
title: str
lastEditedBy: str = None
sortTitle: str = ""
subtitle: str = ""
description: str = ""
@ -25,10 +36,6 @@ class Book(ActivityObject):
firstPublishedDate: str = ""
publishedDate: str = ""
openlibraryKey: str = ""
librarythingKey: str = ""
goodreadsKey: str = ""
cover: Document = None
type: str = "Book"
@ -55,23 +62,21 @@ class Work(Book):
"""work instance of a book object"""
lccn: str = ""
defaultEdition: str = ""
editions: List[str] = field(default_factory=lambda: [])
type: str = "Work"
@dataclass(init=False)
class Author(ActivityObject):
class Author(BookData):
"""author of a book"""
name: str
lastEditedBy: str = None
isni: str = None
viafId: str = None
gutenbergId: str = None
born: str = None
died: str = None
aliases: List[str] = field(default_factory=lambda: [])
bio: str = ""
openlibraryKey: str = ""
librarythingKey: str = ""
goodreadsKey: str = ""
wikipediaLink: str = ""
type: str = "Author"

View file

@ -83,4 +83,5 @@ class Rating(Comment):
rating: int
content: str = None
name: str = None # not used, but the model inherits from Review
type: str = "Rating"

View file

@ -2,7 +2,6 @@
from django.contrib import admin
from bookwyrm import models
admin.site.register(models.SiteSettings)
admin.site.register(models.User)
admin.site.register(models.FederatedServer)
admin.site.register(models.Connector)

View file

@ -30,7 +30,6 @@ class AbstractMinimalConnector(ABC):
"covers_url",
"search_url",
"isbn_search_url",
"max_query_count",
"name",
"identifier",
"local",
@ -44,7 +43,7 @@ class AbstractMinimalConnector(ABC):
if min_confidence:
params["min_confidence"] = min_confidence
data = get_data(
data = self.get_search_data(
"%s%s" % (self.search_url, query),
params=params,
)
@ -57,7 +56,7 @@ class AbstractMinimalConnector(ABC):
def isbn_search(self, query):
"""isbn search"""
params = {}
data = get_data(
data = self.get_search_data(
"%s%s" % (self.isbn_search_url, query),
params=params,
)
@ -68,6 +67,10 @@ class AbstractMinimalConnector(ABC):
results.append(self.format_isbn_search_result(doc))
return results
def get_search_data(self, remote_id, **kwargs): # pylint: disable=no-self-use
"""this allows connectors to override the default behavior"""
return get_data(remote_id, **kwargs)
@abstractmethod
def get_or_create_book(self, remote_id):
"""pull up a book record by whatever means possible"""
@ -98,13 +101,6 @@ class AbstractConnector(AbstractMinimalConnector):
# title we handle separately.
self.book_mappings = []
def is_available(self):
"""check if you're allowed to use this connector"""
if self.max_query_count is not None:
if self.connector.query_count >= self.max_query_count:
return False
return True
def get_or_create_book(self, remote_id):
"""translate arbitrary json into an Activitypub dataclass"""
# first, check if we have the origin_id saved
@ -112,13 +108,12 @@ class AbstractConnector(AbstractMinimalConnector):
remote_id
) or models.Work.find_existing_by_remote_id(remote_id)
if existing:
if hasattr(existing, "get_default_editon"):
return existing.get_default_editon()
if hasattr(existing, "default_edition"):
return existing.default_edition
return existing
# load the json
data = get_data(remote_id)
mapped_data = dict_from_mappings(data, self.book_mappings)
data = self.get_book_data(remote_id)
if self.is_work_data(data):
try:
edition_data = self.get_edition_from_work_data(data)
@ -126,30 +121,36 @@ class AbstractConnector(AbstractMinimalConnector):
# hack: re-use the work data as the edition data
# this is why remote ids aren't necessarily unique
edition_data = data
work_data = mapped_data
work_data = data
else:
edition_data = data
try:
work_data = self.get_work_from_edition_data(data)
work_data = dict_from_mappings(work_data, self.book_mappings)
except (KeyError, ConnectorException):
work_data = mapped_data
edition_data = data
except (KeyError, ConnectorException) as e:
logger.exception(e)
work_data = data
if not work_data or not edition_data:
raise ConnectorException("Unable to load book data: %s" % remote_id)
with transaction.atomic():
# create activitypub object
work_activity = activitypub.Work(**work_data)
work_activity = activitypub.Work(
**dict_from_mappings(work_data, self.book_mappings)
)
# this will dedupe automatically
work = work_activity.to_model(model=models.Work)
for author in self.get_authors_from_data(data):
for author in self.get_authors_from_data(work_data):
work.authors.add(author)
edition = self.create_edition_from_data(work, edition_data)
load_more_data.delay(self.connector.id, work.id)
return edition
def get_book_data(self, remote_id): # pylint: disable=no-self-use
"""this allows connectors to override the default behavior"""
return get_data(remote_id)
def create_edition_from_data(self, work, edition_data):
"""if we already have the work, we're ready"""
mapped_data = dict_from_mappings(edition_data, self.book_mappings)
@ -159,10 +160,6 @@ class AbstractConnector(AbstractMinimalConnector):
edition.connector = self.connector
edition.save()
if not work.default_edition:
work.default_edition = edition
work.save()
for author in self.get_authors_from_data(edition_data):
edition.authors.add(author)
if not edition.authors.exists() and work.authors.exists():
@ -176,7 +173,7 @@ class AbstractConnector(AbstractMinimalConnector):
if existing:
return existing
data = get_data(remote_id)
data = self.get_book_data(remote_id)
mapped_data = dict_from_mappings(data, self.author_mappings)
try:
@ -213,6 +210,10 @@ def dict_from_mappings(data, mappings):
the subclass"""
result = {}
for mapping in mappings:
# sometimes there are multiple mappings for one field, don't
# overwrite earlier writes in that case
if mapping.local_field in result and result[mapping.local_field]:
continue
result[mapping.local_field] = mapping.get_value(data)
return result
@ -273,6 +274,7 @@ class SearchResult:
title: str
key: str
connector: object
view_link: str = None
author: str = None
year: str = None
cover: str = None

View file

@ -7,11 +7,7 @@ class Connector(AbstractMinimalConnector):
"""this is basically just for search"""
def get_or_create_book(self, remote_id):
edition = activitypub.resolve_remote_id(remote_id, model=models.Edition)
work = edition.parent_work
work.default_edition = work.get_default_edition()
work.save()
return edition
return activitypub.resolve_remote_id(remote_id, model=models.Edition)
def parse_search_data(self, data):
return data

View file

@ -19,7 +19,7 @@ class ConnectorException(HTTPError):
"""when the connector can't do what was asked"""
def search(query, min_confidence=0.1):
def search(query, min_confidence=0.1, return_first=False):
"""find books based on arbitary keywords"""
if not query:
return []
@ -29,23 +29,18 @@ def search(query, min_confidence=0.1):
isbn = re.sub(r"[\W_]", "", query)
maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13
dedup_slug = lambda r: "%s/%s/%s" % (r.title, r.author, r.year)
result_index = set()
for connector in get_connectors():
result_set = None
if maybe_isbn:
if maybe_isbn and connector.isbn_search_url and connector.isbn_search_url == "":
# Search on ISBN
if not connector.isbn_search_url or connector.isbn_search_url == "":
result_set = []
else:
try:
result_set = connector.isbn_search(isbn)
except Exception as e: # pylint: disable=broad-except
logger.exception(e)
continue
# if this fails, we can still try regular search
# if no isbn search or results, we fallback to generic search
if result_set in (None, []):
# 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 e: # pylint: disable=broad-except
@ -53,10 +48,11 @@ def search(query, min_confidence=0.1):
logger.exception(e)
continue
# if the search results look the same, ignore them
result_set = [r for r in result_set if dedup_slug(r) not in result_index]
# `|=` concats two sets. WE ARE GETTING FANCY HERE
result_index |= set(dedup_slug(r) for r in result_set)
if return_first and result_set:
# if we found anything, return it
return result_set[0]
if result_set or connector.local:
results.append(
{
"connector": connector,
@ -64,13 +60,18 @@ def search(query, min_confidence=0.1):
}
)
if return_first:
return None
return results
def local_search(query, min_confidence=0.1, raw=False):
def local_search(query, min_confidence=0.1, raw=False, filters=None):
"""only look at local search results"""
connector = load_connector(models.Connector.objects.get(local=True))
return connector.search(query, min_confidence=min_confidence, raw=raw)
return connector.search(
query, min_confidence=min_confidence, raw=raw, filters=filters
)
def isbn_local_search(query, raw=False):
@ -81,16 +82,12 @@ def isbn_local_search(query, raw=False):
def first_search_result(query, min_confidence=0.1):
"""search until you find a result that fits"""
for connector in get_connectors():
result = connector.search(query, min_confidence=min_confidence)
if result:
return result[0]
return None
return search(query, min_confidence=min_confidence, return_first=True) or None
def get_connectors():
"""load all connectors"""
for info in models.Connector.objects.order_by("priority").all():
for info in models.Connector.objects.filter(active=True).order_by("priority").all():
yield load_connector(info)

View file

@ -0,0 +1,232 @@
""" inventaire data connector """
import re
from bookwyrm import models
from .abstract_connector import AbstractConnector, SearchResult, Mapping
from .abstract_connector import get_data
from .connector_manager import ConnectorException
class Connector(AbstractConnector):
"""instantiate a connector for OL"""
def __init__(self, identifier):
super().__init__(identifier)
get_first = lambda a: a[0]
shared_mappings = [
Mapping("id", remote_field="uri", formatter=self.get_remote_id),
Mapping("bnfId", remote_field="wdt:P268", formatter=get_first),
Mapping("openlibraryKey", remote_field="wdt:P648", formatter=get_first),
]
self.book_mappings = [
Mapping("title", remote_field="wdt:P1476", formatter=get_first),
Mapping("title", remote_field="labels", formatter=get_language_code),
Mapping("subtitle", remote_field="wdt:P1680", formatter=get_first),
Mapping("inventaireId", remote_field="uri"),
Mapping(
"description", remote_field="sitelinks", formatter=self.get_description
),
Mapping("cover", remote_field="image", formatter=self.get_cover_url),
Mapping("isbn13", remote_field="wdt:P212", formatter=get_first),
Mapping("isbn10", remote_field="wdt:P957", formatter=get_first),
Mapping("oclcNumber", remote_field="wdt:P5331", formatter=get_first),
Mapping("goodreadsKey", remote_field="wdt:P2969", formatter=get_first),
Mapping("librarythingKey", remote_field="wdt:P1085", formatter=get_first),
Mapping("languages", remote_field="wdt:P407", formatter=self.resolve_keys),
Mapping("publishers", remote_field="wdt:P123", formatter=self.resolve_keys),
Mapping("publishedDate", remote_field="wdt:P577", formatter=get_first),
Mapping("pages", remote_field="wdt:P1104", formatter=get_first),
Mapping(
"subjectPlaces", remote_field="wdt:P840", formatter=self.resolve_keys
),
Mapping("subjects", remote_field="wdt:P921", formatter=self.resolve_keys),
Mapping("asin", remote_field="wdt:P5749", formatter=get_first),
] + shared_mappings
# TODO: P136: genre, P674 characters, P950 bne
self.author_mappings = [
Mapping("id", remote_field="uri", formatter=self.get_remote_id),
Mapping("name", remote_field="labels", formatter=get_language_code),
Mapping("bio", remote_field="sitelinks", formatter=self.get_description),
Mapping("goodreadsKey", remote_field="wdt:P2963", formatter=get_first),
Mapping("isni", remote_field="wdt:P213", formatter=get_first),
Mapping("viafId", remote_field="wdt:P214", formatter=get_first),
Mapping("gutenberg_id", remote_field="wdt:P1938", formatter=get_first),
Mapping("born", remote_field="wdt:P569", formatter=get_first),
Mapping("died", remote_field="wdt:P570", formatter=get_first),
] + shared_mappings
def get_remote_id(self, value):
"""convert an id/uri into a url"""
return "{:s}?action=by-uris&uris={:s}".format(self.books_url, value)
def get_book_data(self, remote_id):
data = get_data(remote_id)
extracted = list(data.get("entities").values())
try:
data = extracted[0]
except KeyError:
raise ConnectorException("Invalid book data")
# flatten the data so that images, uri, and claims are on the same level
return {
**data.get("claims", {}),
**{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks"]},
}
def search(self, query, min_confidence=None):
"""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 = (
"{:s}/img/entities/{:s}".format(self.covers_url, 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="{:s}/entity/{:s}".format(
self.base_url, 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="{:s}/entity/{:s}".format(
self.base_url, 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"
def load_edition_data(self, work_uri):
"""get a list of editions for a work"""
url = (
"{:s}?action=reverse-claims&property=wdt:P629&value={:s}&sort=true".format(
self.books_url, work_uri
)
)
return get_data(url)
def get_edition_from_work_data(self, data):
data = self.load_edition_data(data.get("uri"))
try:
uri = data["uris"][0]
except KeyError:
raise ConnectorException("Invalid book data")
return self.get_book_data(self.get_remote_id(uri))
def get_work_from_edition_data(self, data):
uri = data.get("wdt:P629", [None])[0]
if not uri:
raise ConnectorException("Invalid book data")
return self.get_book_data(self.get_remote_id(uri))
def get_authors_from_data(self, data):
authors = data.get("wdt:P50", [])
for author in authors:
yield self.get_or_create_author(self.get_remote_id(author))
def expand_book_data(self, book):
work = book
# go from the edition to the work, if necessary
if isinstance(book, models.Edition):
work = book.parent_work
try:
edition_options = self.load_edition_data(work.inventaire_id)
except ConnectorException:
# who knows, man
return
for edition_uri in edition_options.get("uris"):
remote_id = self.get_remote_id(edition_uri)
try:
data = self.get_book_data(remote_id)
except ConnectorException:
# who, indeed, knows
continue
self.create_edition_from_data(work, data)
def get_cover_url(self, cover_blob, *_):
"""format the relative cover url into an absolute one:
{"url": "/img/entities/e794783f01b9d4f897a1ea9820b96e00d346994f"}
"""
# covers may or may not be a list
if isinstance(cover_blob, list) and len(cover_blob) > 0:
cover_blob = cover_blob[0]
cover_id = cover_blob.get("url")
if not cover_id:
return None
# cover may or may not be an absolute url already
if re.match(r"^http", cover_id):
return cover_id
return "%s%s" % (self.covers_url, cover_id)
def resolve_keys(self, keys):
"""cool, it's "wd:Q3156592" now what the heck does that mean"""
results = []
for uri in keys:
try:
data = self.get_book_data(self.get_remote_id(uri))
except ConnectorException:
continue
results.append(get_language_code(data.get("labels")))
return results
def get_description(self, links):
"""grab an extracted excerpt from wikipedia"""
link = links.get("enwiki")
if not link:
return ""
url = "{:s}/api/data?action=wp-extract&lang=en&title={:s}".format(
self.base_url, link
)
try:
data = get_data(url)
except ConnectorException:
return ""
return data.get("extract")
def get_language_code(options, code="en"):
"""when there are a bunch of translation but we need a single field"""
result = options.get(code)
if result:
return result
values = list(options.values())
return values[0] if values else None

View file

@ -14,8 +14,8 @@ class Connector(AbstractConnector):
def __init__(self, identifier):
super().__init__(identifier)
get_first = lambda a: a[0]
get_remote_id = lambda a: self.base_url + a
get_first = lambda a, *args: a[0]
get_remote_id = lambda a, *args: self.base_url + a
self.book_mappings = [
Mapping("title"),
Mapping("id", remote_field="key", formatter=get_remote_id),
@ -58,6 +58,13 @@ class Connector(AbstractConnector):
Mapping("bio", formatter=get_description),
]
def get_book_data(self, remote_id):
data = get_data(remote_id)
if data.get("type", {}).get("key") == "/type/redirect":
remote_id = self.base_url + data.get("location")
return get_data(remote_id)
return data
def get_remote_id_from_data(self, data):
"""format a url from an openlibrary id field"""
try:
@ -75,8 +82,11 @@ class Connector(AbstractConnector):
except KeyError:
raise ConnectorException("Invalid book data")
url = "%s%s/editions" % (self.books_url, key)
data = get_data(url)
return pick_default_edition(data["entries"])
data = self.get_book_data(url)
edition = pick_default_edition(data["entries"])
if not edition:
raise ConnectorException("No editions for work")
return edition
def get_work_from_edition_data(self, data):
try:
@ -84,7 +94,7 @@ class Connector(AbstractConnector):
except (IndexError, KeyError):
raise ConnectorException("No work found for edition")
url = "%s%s" % (self.books_url, key)
return get_data(url)
return self.get_book_data(url)
def get_authors_from_data(self, data):
"""parse author json and load or create authors"""
@ -143,7 +153,7 @@ class Connector(AbstractConnector):
def load_edition_data(self, olkey):
"""query openlibrary for editions of a work"""
url = "%s/works/%s/editions" % (self.books_url, olkey)
return get_data(url)
return self.get_book_data(url)
def expand_book_data(self, book):
work = book

View file

@ -3,7 +3,7 @@ from functools import reduce
import operator
from django.contrib.postgres.search import SearchRank, SearchVector
from django.db.models import Count, F, Q
from django.db.models import Count, OuterRef, Subquery, F, Q
from bookwyrm import models
from .abstract_connector import AbstractConnector, SearchResult
@ -13,15 +13,16 @@ class Connector(AbstractConnector):
"""instantiate a connector"""
# pylint: disable=arguments-differ
def search(self, query, min_confidence=0.1, raw=False):
def search(self, query, min_confidence=0.1, raw=False, filters=None):
"""search your local database"""
filters = filters or []
if not query:
return []
# first, try searching unqiue identifiers
results = search_identifiers(query)
results = search_identifiers(query, *filters)
if not results:
# then try searching title/author
results = search_title_author(query, min_confidence)
results = search_title_author(query, min_confidence, *filters)
search_results = []
for result in results:
if raw:
@ -46,7 +47,16 @@ class Connector(AbstractConnector):
# when there are multiple editions of the same work, pick the default.
# it would be odd for this to happen.
results = results.filter(parent_work__default_edition__id=F("id")) or results
default_editions = models.Edition.objects.filter(
parent_work=OuterRef("parent_work")
).order_by("-edition_rank")
results = (
results.annotate(
default_id=Subquery(default_editions.values("id")[:1])
).filter(default_id=F("id"))
or results
)
search_results = []
for result in results:
@ -59,6 +69,10 @@ class Connector(AbstractConnector):
return search_results
def format_search_result(self, search_result):
cover = None
if search_result.cover:
cover = "%s%s" % (self.covers_url, search_result.cover)
return SearchResult(
title=search_result.title,
key=search_result.remote_id,
@ -67,7 +81,7 @@ class Connector(AbstractConnector):
if search_result.published_date
else None,
connector=self,
cover="%s%s" % (self.covers_url, search_result.cover),
cover=cover,
confidence=search_result.rank if hasattr(search_result, "rank") else 1,
)
@ -98,23 +112,31 @@ class Connector(AbstractConnector):
pass
def search_identifiers(query):
def search_identifiers(query, *filters):
"""tries remote_id, isbn; defined as dedupe fields on the model"""
filters = [
or_filters = [
{f.name: query}
for f in models.Edition._meta.get_fields()
if hasattr(f, "deduplication_field") and f.deduplication_field
]
results = models.Edition.objects.filter(
reduce(operator.or_, (Q(**f) for f in filters))
*filters, reduce(operator.or_, (Q(**f) for f in or_filters))
).distinct()
# when there are multiple editions of the same work, pick the default.
# it would be odd for this to happen.
return results.filter(parent_work__default_edition__id=F("id")) or results
default_editions = models.Edition.objects.filter(
parent_work=OuterRef("parent_work")
).order_by("-edition_rank")
return (
results.annotate(default_id=Subquery(default_editions.values("id")[:1])).filter(
default_id=F("id")
)
or results
)
def search_title_author(query, min_confidence):
def search_title_author(query, min_confidence, *filters):
"""searches for title and author"""
vector = (
SearchVector("title", weight="A")
@ -126,7 +148,7 @@ def search_title_author(query, min_confidence):
results = (
models.Edition.objects.annotate(search=vector)
.annotate(rank=SearchRank(vector, query))
.filter(rank__gt=min_confidence)
.filter(*filters, rank__gt=min_confidence)
.order_by("-rank")
)
@ -139,10 +161,10 @@ def search_title_author(query, min_confidence):
for work_id in set(editions_of_work):
editions = results.filter(parent_work=work_id)
default = editions.filter(parent_work__default_edition=F("id"))
default_rank = default.first().rank if default.exists() else 0
default = editions.order_by("-edition_rank").first()
default_rank = default.rank if default else 0
# if mutliple books have the top rank, pick the default edition
if default_rank == editions.first().rank:
yield default.first()
yield default
else:
yield editions.first()

View file

@ -1,3 +1,3 @@
""" settings book data connectors """
CONNECTORS = ["openlibrary", "self_connector", "bookwyrm_connector"]
CONNECTORS = ["openlibrary", "inventaire", "self_connector", "bookwyrm_connector"]

View file

@ -4,4 +4,7 @@ from bookwyrm import models
def site_settings(request): # pylint: disable=unused-argument
"""include the custom info about the site"""
return {"site": models.SiteSettings.objects.get()}
return {
"site": models.SiteSettings.objects.get(),
"active_announcements": models.Announcement.active_announcements(),
}

View file

@ -269,6 +269,12 @@ class SiteForm(CustomForm):
exclude = []
class AnnouncementForm(CustomForm):
class Meta:
model = models.Announcement
exclude = ["remote_id"]
class ListForm(CustomForm):
class Meta:
model = models.List

View file

@ -3,3 +3,4 @@
from .importer import Importer
from .goodreads_import import GoodreadsImporter
from .librarything_import import LibrarythingImporter
from .storygraph_import import StorygraphImporter

View file

@ -116,6 +116,10 @@ def handle_imported_book(source, user, item, include_reviews, privacy):
read.save()
if include_reviews and (item.rating or item.review):
# we don't know the publication date of the review,
# but "now" is a bad guess
published_date_guess = item.date_read or item.date_added
if item.review:
review_title = (
"Review of {!r} on {!r}".format(
item.book.title,
@ -124,10 +128,6 @@ def handle_imported_book(source, user, item, include_reviews, privacy):
if item.review
else ""
)
# we don't know the publication date of the review,
# but "now" is a bad guess
published_date_guess = item.date_read or item.date_added
models.Review.objects.create(
user=user,
book=item.book,
@ -137,3 +137,12 @@ def handle_imported_book(source, user, item, include_reviews, privacy):
published_date=published_date_guess,
privacy=privacy,
)
else:
# just a rating
models.ReviewRating.objects.create(
user=user,
book=item.book,
rating=item.rating,
published_date=published_date_guess,
privacy=privacy,
)

View file

@ -0,0 +1,34 @@
""" handle reading a csv from librarything """
import re
import math
from . import Importer
class StorygraphImporter(Importer):
"""csv downloads from librarything"""
service = "Storygraph"
# mandatory_fields : fields matching the book title and author
mandatory_fields = ["Title"]
def parse_fields(self, entry):
"""custom parsing for storygraph"""
data = {}
data["import_source"] = self.service
data["Title"] = entry["Title"]
data["Author"] = entry["Authors"] if "Authors" in entry else entry["Author"]
data["ISBN13"] = entry["ISBN"]
data["My Review"] = entry["Review"]
if entry["Star Rating"]:
data["My Rating"] = math.ceil(float(entry["Star Rating"]))
else:
data["My Rating"] = ""
data["Date Added"] = re.sub(r"[/]", "-", entry["Date Added"])
data["Date Read"] = re.sub(r"[/]", "-", entry["Last Date Read"])
data["Exclusive Shelf"] = (
{"read": "read", "currently-reading": "reading", "to-read": "to-read"}
).get(entry["Read Status"], None)
return data

View file

@ -1,4 +1,5 @@
from django.core.management.base import BaseCommand, CommandError
""" What you need in the database to make it work """
from django.core.management.base import BaseCommand
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
@ -7,12 +8,14 @@ from bookwyrm.settings import DOMAIN
def init_groups():
"""permission levels"""
groups = ["admin", "moderator", "editor"]
for group in groups:
Group.objects.create(name=group)
def init_permissions():
"""permission types"""
permissions = [
{
"codename": "edit_instance_settings",
@ -69,6 +72,7 @@ def init_permissions():
def init_connectors():
"""access book data sources"""
Connector.objects.create(
identifier=DOMAIN,
name="Local",
@ -94,6 +98,18 @@ def init_connectors():
priority=2,
)
Connector.objects.create(
identifier="inventaire.io",
name="Inventaire",
connector_file="inventaire",
base_url="https://inventaire.io",
books_url="https://inventaire.io/api/entities",
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,
)
Connector.objects.create(
identifier="openlibrary.org",
name="OpenLibrary",
@ -118,7 +134,11 @@ def init_federated_servers():
def init_settings():
SiteSettings.objects.create()
"""info about the instance"""
SiteSettings.objects.create(
support_link="https://www.patreon.com/bookwyrm",
support_title="Patreon",
)
class Command(BaseCommand):

View file

@ -0,0 +1,30 @@
# Generated by Django 3.1.6 on 2021-04-06 17:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0061_auto_20210402_1435"),
]
operations = [
migrations.RemoveConstraint(
model_name="connector",
name="connector_file_valid",
),
migrations.AlterField(
model_name="connector",
name="connector_file",
field=models.CharField(
choices=[
("openlibrary", "Openlibrary"),
("inventaire", "Inventaire"),
("self_connector", "Self Connector"),
("bookwyrm_connector", "Bookwyrm Connector"),
],
max_length=255,
),
),
]

View file

@ -0,0 +1,63 @@
# Generated by Django 3.1.6 on 2021-04-07 00:45
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0062_auto_20210406_1731"),
]
operations = [
migrations.AddField(
model_name="author",
name="bnf_id",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
migrations.AddField(
model_name="author",
name="gutenberg_id",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
migrations.AddField(
model_name="author",
name="inventaire_id",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
migrations.AddField(
model_name="author",
name="isni",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
migrations.AddField(
model_name="author",
name="viaf_id",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
migrations.AddField(
model_name="book",
name="bnf_id",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
migrations.AddField(
model_name="book",
name="inventaire_id",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2 on 2021-04-26 21:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0063_auto_20210407_0045"),
("bookwyrm", "0070_auto_20210423_0121"),
]
operations = []

View file

@ -0,0 +1,17 @@
# Generated by Django 3.2 on 2021-04-28 22:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0071_merge_0063_auto_20210407_0045_0070_auto_20210423_0121"),
]
operations = [
migrations.RemoveField(
model_name="work",
name="default_edition",
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2 on 2021-04-30 17:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0072_remove_work_default_edition"),
]
operations = [
migrations.AddField(
model_name="sitesettings",
name="footer_item",
field=models.TextField(blank=True, null=True),
),
]

View file

@ -0,0 +1,48 @@
# Generated by Django 3.2 on 2021-05-11 18:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0073_sitesettings_footer_item"),
]
operations = [
migrations.RemoveField(
model_name="connector",
name="max_query_count",
),
migrations.RemoveField(
model_name="connector",
name="politeness_delay",
),
migrations.RemoveField(
model_name="connector",
name="query_count",
),
migrations.RemoveField(
model_name="connector",
name="query_count_expiry",
),
migrations.AddField(
model_name="connector",
name="active",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="connector",
name="deactivation_reason",
field=models.CharField(
blank=True,
choices=[
("self_deletion", "Self Deletion"),
("moderator_deletion", "Moderator Deletion"),
("domain_block", "Domain Block"),
],
max_length=255,
null=True,
),
),
]

View file

@ -0,0 +1,56 @@
# Generated by Django 3.2 on 2021-05-20 19:34
import bookwyrm.models.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0074_auto_20210511_1829"),
]
operations = [
migrations.CreateModel(
name="Announcement",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_date", models.DateTimeField(auto_now_add=True)),
("updated_date", models.DateTimeField(auto_now=True)),
(
"remote_id",
bookwyrm.models.fields.RemoteIdField(
max_length=255,
null=True,
validators=[bookwyrm.models.fields.validate_remote_id],
),
),
("preview", models.CharField(max_length=255)),
("content", models.TextField(blank=True, null=True)),
("event_date", models.DateTimeField(blank=True, null=True)),
("start_date", models.DateTimeField(blank=True, null=True)),
("end_date", models.DateTimeField(blank=True, null=True)),
("active", models.BooleanField(default=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
]

View file

@ -25,6 +25,7 @@ from .federated_server import FederatedServer
from .import_job import ImportJob, ImportItem
from .site import SiteSettings, SiteInvite, PasswordReset, InviteRequest
from .announcement import Announcement
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
activity_models = {

View file

@ -0,0 +1,28 @@
""" admin announcements """
from django.db import models
from django.db.models import Q
from django.utils import timezone
from .base_model import BookWyrmModel
class Announcement(BookWyrmModel):
"""The admin has something to say"""
user = models.ForeignKey("User", on_delete=models.PROTECT)
preview = models.CharField(max_length=255)
content = models.TextField(null=True, blank=True)
event_date = models.DateTimeField(blank=True, null=True)
start_date = models.DateTimeField(blank=True, null=True)
end_date = models.DateTimeField(blank=True, null=True)
active = models.BooleanField(default=True)
@classmethod
def active_announcements(cls):
"""announcements that should be displayed"""
now = timezone.now()
return cls.objects.filter(
Q(start_date__isnull=True) | Q(start_date__lte=now),
Q(end_date__isnull=True) | Q(end_date__gte=now),
active=True,
)

View file

@ -14,6 +14,15 @@ class Author(BookDataModel):
wikipedia_link = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
isni = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
viaf_id = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
gutenberg_id = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
# idk probably other keys would be useful here?
born = fields.DateTimeField(blank=True, null=True)
died = fields.DateTimeField(blank=True, null=True)

View file

@ -6,6 +6,16 @@ from bookwyrm.settings import DOMAIN
from .fields import RemoteIdField
DeactivationReason = models.TextChoices(
"DeactivationReason",
[
"self_deletion",
"moderator_deletion",
"domain_block",
],
)
class BookWyrmModel(models.Model):
"""shared fields"""

View file

@ -1,11 +1,11 @@
""" database schema for books and shelves """
import re
from django.db import models, transaction
from django.db import models
from model_utils.managers import InheritanceManager
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import DOMAIN, DEFAULT_LANGUAGE
from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
from .base_model import BookWyrmModel
@ -19,12 +19,18 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
openlibrary_key = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
inventaire_id = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
librarything_key = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
goodreads_key = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
bnf_id = fields.CharField( # Bibliothèque nationale de France
max_length=255, blank=True, null=True, deduplication_field=True
)
last_edited_by = fields.ForeignKey(
"User",
@ -137,10 +143,6 @@ class Work(OrderedCollectionPageMixin, Book):
lccn = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
# this has to be nullable but should never be null
default_edition = fields.ForeignKey(
"Edition", on_delete=models.PROTECT, null=True, load_remote=False
)
def save(self, *args, **kwargs):
"""set some fields on the edition object"""
@ -149,18 +151,10 @@ class Work(OrderedCollectionPageMixin, Book):
edition.save()
return super().save(*args, **kwargs)
def get_default_edition(self):
@property
def default_edition(self):
"""in case the default edition is not set"""
return self.default_edition or self.editions.order_by("-edition_rank").first()
@transaction.atomic()
def reset_default_edition(self):
"""sets a new default edition based on computed rank"""
self.default_edition = None
# editions are re-ranked implicitly
self.save()
self.default_edition = self.get_default_edition()
self.save()
return self.editions.order_by("-edition_rank").first()
def to_edition_list(self, **kwargs):
"""an ordered collection of editions"""
@ -214,17 +208,20 @@ class Edition(Book):
activity_serializer = activitypub.Edition
name_field = "title"
def get_rank(self, ignore_default=False):
def get_rank(self):
"""calculate how complete the data is on this edition"""
if (
not ignore_default
and self.parent_work
and self.parent_work.default_edition == self
):
# default edition has the highest rank
return 20
rank = 0
# big ups for havinga cover
rank += int(bool(self.cover)) * 3
# is it in the instance's preferred language?
rank += int(bool(DEFAULT_LANGUAGE in self.languages))
# prefer print editions
if self.physical_format:
rank += int(
bool(self.physical_format.lower() in ["paperback", "hardcover"])
)
# does it have metadata?
rank += int(bool(self.isbn_13))
rank += int(bool(self.isbn_10))
rank += int(bool(self.oclc_number))
@ -242,6 +239,12 @@ class Edition(Book):
if self.isbn_10 and not self.isbn_13:
self.isbn_13 = isbn_10_to_13(self.isbn_10)
# normalize isbn format
if self.isbn_10:
self.isbn_10 = re.sub(r"[^0-9X]", "", self.isbn_10)
if self.isbn_13:
self.isbn_13 = re.sub(r"[^0-9X]", "", self.isbn_13)
# set rank
self.edition_rank = self.get_rank()

View file

@ -2,7 +2,7 @@
from django.db import models
from bookwyrm.connectors.settings import CONNECTORS
from .base_model import BookWyrmModel
from .base_model import BookWyrmModel, DeactivationReason
ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS)
@ -17,6 +17,10 @@ class Connector(BookWyrmModel):
local = models.BooleanField(default=False)
connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)
api_key = models.CharField(max_length=255, null=True, blank=True)
active = models.BooleanField(default=True)
deactivation_reason = models.CharField(
max_length=255, choices=DeactivationReason.choices, null=True, blank=True
)
base_url = models.CharField(max_length=255)
books_url = models.CharField(max_length=255)
@ -24,23 +28,6 @@ class Connector(BookWyrmModel):
search_url = models.CharField(max_length=255, null=True, blank=True)
isbn_search_url = models.CharField(max_length=255, null=True, blank=True)
politeness_delay = models.IntegerField(null=True, blank=True) # seconds
max_query_count = models.IntegerField(null=True, blank=True)
# how many queries executed in a unit of time, like a day
query_count = models.IntegerField(default=0)
# when to reset the query count back to 0 (ie, after 1 day)
query_count_expiry = models.DateTimeField(auto_now_add=True, blank=True)
class Meta:
"""check that there's code to actually use this connector"""
constraints = [
models.CheckConstraint(
check=models.Q(connector_file__in=ConnectorFiles),
name="connector_file_valid",
)
]
def __str__(self):
return "{} ({})".format(
self.identifier,

View file

@ -1,5 +1,6 @@
""" connections to external ActivityPub servers """
from urllib.parse import urlparse
from django.apps import apps
from django.db import models
from .base_model import BookWyrmModel
@ -34,6 +35,13 @@ class FederatedServer(BookWyrmModel):
is_active=False, deactivation_reason="domain_block"
)
# check for related connectors
if self.application_type == "bookwyrm":
connector_model = apps.get_model("bookwyrm.Connector", require_ready=True)
connector_model.objects.filter(
identifier=self.server_name, active=True
).update(active=False, deactivation_reason="domain_block")
def unblock(self):
"""unblock a server"""
self.status = "federated"
@ -43,6 +51,15 @@ class FederatedServer(BookWyrmModel):
is_active=True, deactivation_reason=None
)
# check for related connectors
if self.application_type == "bookwyrm":
connector_model = apps.get_model("bookwyrm.Connector", require_ready=True)
connector_model.objects.filter(
identifier=self.server_name,
active=False,
deactivation_reason="domain_block",
).update(active=True, deactivation_reason=None)
@classmethod
def is_blocked(cls, url):
"""look up if a domain is blocked"""

View file

@ -128,7 +128,9 @@ class ImportItem(models.Model):
@property
def rating(self):
"""x/5 star rating for a book"""
if self.data.get("My Rating", None):
return int(self.data["My Rating"])
return None
@property
def date_added(self):

View file

@ -19,19 +19,28 @@ class SiteSettings(models.Model):
max_length=150, default="Social Reading and Reviewing"
)
instance_description = models.TextField(default="This instance has no description.")
# about page
registration_closed_text = models.TextField(
default="Contact an administrator to get an invite"
)
code_of_conduct = models.TextField(default="Add a code of conduct here.")
privacy_policy = models.TextField(default="Add a privacy policy here.")
# registration
allow_registration = models.BooleanField(default=True)
allow_invite_requests = models.BooleanField(default=True)
# images
logo = models.ImageField(upload_to="logos/", null=True, blank=True)
logo_small = models.ImageField(upload_to="logos/", null=True, blank=True)
favicon = models.ImageField(upload_to="logos/", null=True, blank=True)
# footer
support_link = models.CharField(max_length=255, null=True, blank=True)
support_title = models.CharField(max_length=100, null=True, blank=True)
admin_email = models.EmailField(max_length=255, null=True, blank=True)
footer_item = models.TextField(null=True, blank=True)
@classmethod
def get(cls):

View file

@ -19,21 +19,11 @@ from bookwyrm.signatures import create_key_pair
from bookwyrm.tasks import app
from bookwyrm.utils import regex
from .activitypub_mixin import OrderedCollectionPageMixin, ActivitypubMixin
from .base_model import BookWyrmModel
from .base_model import BookWyrmModel, DeactivationReason
from .federated_server import FederatedServer
from . import fields, Review
DeactivationReason = models.TextChoices(
"DeactivationReason",
[
"self_deletion",
"moderator_deletion",
"domain_block",
],
)
class User(OrderedCollectionPageMixin, AbstractUser):
"""a user who wants to read books"""
@ -150,6 +140,19 @@ class User(OrderedCollectionPageMixin, AbstractUser):
"""for consistent naming"""
return not self.is_active
@property
def unread_notification_count(self):
"""count of notifications, for the templates"""
return self.notification_set.filter(read=False).count()
@property
def has_unread_mentions(self):
"""whether any of the unread notifications are conversations"""
return self.notification_set.filter(
read=False,
notification_type__in=["REPLY", "MENTION", "TAG", "REPORT"],
).exists()
activity_serializer = activitypub.Person
@classmethod
@ -359,7 +362,10 @@ class AnnualGoal(BookWyrmModel):
def books(self):
"""the books you've read this year"""
return (
self.user.readthrough_set.filter(finish_date__year__gte=self.year)
self.user.readthrough_set.filter(
finish_date__year__gte=self.year,
finish_date__year__lt=self.year + 1,
)
.order_by("-finish_date")
.all()
)
@ -383,7 +389,8 @@ class AnnualGoal(BookWyrmModel):
def book_count(self):
"""how many books you've read this year"""
return self.user.readthrough_set.filter(
finish_date__year__gte=self.year
finish_date__year__gte=self.year,
finish_date__year__lt=self.year + 1,
).count()

View file

@ -11,6 +11,7 @@ DOMAIN = env("DOMAIN")
VERSION = "0.0.1"
PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
# celery
CELERY_BROKER = env("CELERY_BROKER")
@ -34,8 +35,10 @@ LOCALE_PATHS = [
os.path.join(BASE_DIR, "locale"),
]
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env("SECRET_KEY")
@ -104,7 +107,7 @@ MAX_STREAM_LENGTH = int(env("MAX_STREAM_LENGTH", 200))
STREAMS = ["home", "local", "federated"]
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
BOOKWYRM_DATABASE_BACKEND = env("BOOKWYRM_DATABASE_BACKEND", "postgres")
@ -115,7 +118,7 @@ BOOKWYRM_DBS = {
"USER": env("POSTGRES_USER", "fedireads"),
"PASSWORD": env("POSTGRES_PASSWORD", "fedireads"),
"HOST": env("POSTGRES_HOST", ""),
"PORT": 5432,
"PORT": env("POSTGRES_PORT", 5432),
},
}
@ -126,7 +129,7 @@ LOGIN_URL = "/login/"
AUTH_USER_MODEL = "bookwyrm.User"
# Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
@ -145,7 +148,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = "en-us"
LANGUAGES = [
@ -167,7 +170,7 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
# https://docs.djangoproject.com/en/3.2/howto/static-files/
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_URL = "/static/"

View file

@ -43,10 +43,6 @@ body {
white-space: nowrap !important;
width: 0.01em !important;
}
.m-0-mobile {
margin: 0 !important;
}
}
.button.is-transparent {
@ -92,10 +88,11 @@ body {
.transition-y.is-hidden {
display: block !important;
visibility: hidden !important;
height: 0;
width: 0;
margin: 0;
padding: 0;
height: 0 !important;
width: 0 !important;
margin: 0 !important;
padding: 0 !important;
overflow: auto;
}
.transition-x,
@ -132,7 +129,7 @@ body {
*
* \e9d9: filled star
* \e9d7: empty star;
******************************************************************************/
* -------------------------------------------------------------------------- */
.form-rate-stars {
width: max-content;
@ -158,70 +155,67 @@ body {
}
/** Book covers
*
* - .is-cover gives the behaviour of the cover and its surrounding. (optional)
* - .cover-container gives the dimensions and position (for borders, image and other elements).
* - .book-cover is positioned and sized based on its container.
*
* To have the cover within specific dimensions, specify a width or height for
* standard bulmas named breapoints:
*
* `is-(w|h)-(auto|xs|s|m|l|xl|xxl)[-(mobile|tablet|desktop)]`
*
* The cover will be centered horizontally and vertically within those dimensions.
*
* When using `.column.is-N`, add `.is-w-auto` to the container so that the flex
* calculations are not biased by the default `max-content`.
******************************************************************************/
.column.is-cover {
flex-grow: 0 !important;
}
.column.is-cover,
.column.is-cover + .column {
flex-basis: auto !important;
}
.cover-container {
height: 250px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
width: max-content;
max-width: 250px;
max-width: 100%;
overflow: hidden;
}
.cover-container.is-large {
height: max-content;
max-width: 330px;
}
.cover-container.is-large img {
max-height: 500px;
height: auto;
}
.cover-container.is-medium {
height: 150px;
}
.cover-container.is-small {
height: 100px;
}
@media only screen and (max-width: 768px) {
.cover-container {
height: 200px;
width: max-content;
}
.cover-container.is-medium {
height: 100px;
}
}
/* Book cover
* -------------------------------------------------------------------------- */
.book-cover {
height: 100%;
object-fit: scale-down;
display: block;
max-width: 100%;
max-height: 100%;
/* Useful when stretching under-sized images. */
image-rendering: optimizeQuality;
image-rendering: smooth;
}
.no-cover {
position: relative;
white-space: normal;
}
/* Cover caption
* -------------------------------------------------------------------------- */
.no-cover div {
.no-cover .cover_caption {
position: absolute;
padding: 1em;
color: white;
top: 0;
right: 0;
bottom: 0;
left: 0;
text-align: center;
}
.cover-container.is-medium .no-cover div {
font-size: 0.9em;
padding: 0.3em;
}
.cover-container.is-small .no-cover div {
font-size: 0.7em;
padding: 0.1em;
padding: 0.25em;
font-size: 0.75em;
color: white;
background-color: #002549;
}
/** Avatars
@ -232,16 +226,6 @@ body {
display: inline;
}
.is-32x32 {
min-width: 32px;
min-height: 32px;
}
.is-96x96 {
min-width: 96px;
min-height: 96px;
}
/** Statuses: Quotes
*
* \e906: icon-quote-open
@ -346,3 +330,386 @@ body {
display: none;
}
}
/* Dimensions
* @todo These could be in rem.
******************************************************************************/
.is-32x32 {
min-width: 32px !important;
min-height: 32px !important;
}
.is-96x96 {
min-width: 96px !important;
min-height: 96px !important;
}
.is-w-auto {
width: auto !important;
}
.is-w-xs {
width: 80px !important;
}
.is-w-s {
width: 100px !important;
}
.is-w-m {
width: 150px !important;
}
.is-w-l {
width: 200px !important;
}
.is-w-xl {
width: 250px !important;
}
.is-w-xxl {
width: 500px !important;
}
.is-h-xs {
height: 80px !important;
}
.is-h-s {
height: 100px !important;
}
.is-h-m {
height: 150px !important;
}
.is-h-l {
height: 200px !important;
}
.is-h-xl {
height: 250px !important;
}
.is-h-xxl {
height: 500px !important;
}
@media only screen and (max-width: 768px) {
.is-w-auto-mobile {
width: auto !important;
}
.is-w-xs-mobile {
width: 80px !important;
}
.is-w-s-mobile {
width: 100px !important;
}
.is-w-m-mobile {
width: 150px !important;
}
.is-w-l-mobile {
width: 200px !important;
}
.is-w-xl-mobile {
width: 250px !important;
}
.is-w-xxl-mobile {
width: 500px !important;
}
.is-h-xs-mobile {
height: 80px !important;
}
.is-h-s-mobile {
height: 100px !important;
}
.is-h-m-mobile {
height: 150px !important;
}
.is-h-l-mobile {
height: 200px !important;
}
.is-h-xl-mobile {
height: 250px !important;
}
.is-h-xxl-mobile {
height: 500px !important;
}
}
@media only screen and (min-width: 769px) {
.is-w-auto-tablet {
width: auto !important;
}
.is-w-xs-tablet {
width: 80px !important;
}
.is-w-s-tablet {
width: 100px !important;
}
.is-w-m-tablet {
width: 150px !important;
}
.is-w-l-tablet {
width: 200px !important;
}
.is-w-xl-tablet {
width: 250px !important;
}
.is-w-xxl-tablet {
width: 500px !important;
}
.is-h-xs-tablet {
height: 80px !important;
}
.is-h-s-tablet {
height: 100px !important;
}
.is-h-m-tablet {
height: 150px !important;
}
.is-h-l-tablet {
height: 200px !important;
}
.is-h-xl-tablet {
height: 250px !important;
}
.is-h-xxl-tablet {
height: 500px !important;
}
}
@media only screen and (min-width: 1024px) {
.is-w-auto-desktop {
width: auto !important;
}
.is-w-xs-desktop {
width: 80px !important;
}
.is-w-s-desktop {
width: 100px !important;
}
.is-w-m-desktop {
width: 150px !important;
}
.is-w-l-desktop {
width: 200px !important;
}
.is-w-xl-desktop {
width: 250px !important;
}
.is-w-xxl-desktop {
width: 500px !important;
}
.is-h-xs-desktop {
height: 80px !important;
}
.is-h-s-desktop {
height: 100px !important;
}
.is-h-m-desktop {
height: 150px !important;
}
.is-h-l-desktop {
height: 200px !important;
}
.is-h-xl-desktop {
height: 250px !important;
}
.is-h-xxl-desktop {
height: 500px !important;
}
}
/* Alignments
*
* Use them with `.align.to-(c|t|r|b|l)[-(mobile|tablet)]`
******************************************************************************/
/* Flex item position
* -------------------------------------------------------------------------- */
.align {
display: flex !important;
flex-direction: row !important;
}
.align.to-c {
justify-content: center !important;
}
.align.to-t {
align-items: flex-start !important;
}
.align.to-r {
justify-content: flex-end !important;
}
.align.to-b {
align-items: flex-end !important;
}
.align.to-l {
justify-content: flex-start !important;
}
@media screen and (max-width: 768px) {
.align.to-c-mobile {
justify-content: center !important;
}
.align.to-t-mobile {
align-items: flex-start !important;
}
.align.to-r-mobile {
justify-content: flex-end !important;
}
.align.to-b-mobile {
align-items: flex-end !important;
}
.align.to-l-mobile {
justify-content: flex-start !important;
}
}
@media screen and (min-width: 769px) {
.align.to-c-tablet {
justify-content: center !important;
}
.align.to-t-tablet {
align-items: flex-start !important;
}
.align.to-r-tablet {
justify-content: flex-end !important;
}
.align.to-b-tablet {
align-items: flex-end !important;
}
.align.to-l-tablet {
justify-content: flex-start !important;
}
}
/* Spacings
*
* Those are supplementary rules to Bulmas. They follow the same conventions.
* Add those youll need.
******************************************************************************/
.mr-auto {
margin-right: auto !important;
}
.ml-auto {
margin-left: auto !important;
}
@media screen and (max-width: 768px) {
.m-0-mobile {
margin: 0 !important;
}
.mr-auto-mobile {
margin-right: auto !important;
}
.ml-auto-mobile {
margin-left: auto !important;
}
.mt-3-mobile {
margin-top: 0.75rem !important;
}
.ml-3-mobile {
margin-left: 0.75rem !important;
}
.mx-3-mobile {
margin-right: 0.75rem !important;
margin-left: 0.75rem !important;
}
.my-3-mobile {
margin-top: 0.75rem !important;
margin-bottom: 0.75rem !important;
}
}
@media screen and (min-width: 769px) {
.m-0-tablet {
margin: 0 !important;
}
.mr-auto-tablet {
margin-right: auto !important;
}
.ml-auto-tablet {
margin-left: auto !important;
}
.mt-3-tablet {
margin-top: 0.75rem !important;
}
.ml-3-tablet {
margin-left: 0.75rem !important;
}
.mx-3-tablet {
margin-right: 0.75rem !important;
margin-left: 0.75rem !important;
}
.my-3-tablet {
margin-top: 0.75rem !important;
margin-bottom: 0.75rem !important;
}
}

File diff suppressed because one or more lines are too long

View file

@ -97,10 +97,12 @@ let BookWyrm = new class {
updateCountElement(counter, data) {
const currentCount = counter.innerText;
const count = data.count;
const hasMentions = data.has_mentions;
if (count != currentCount) {
this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-hidden', count < 1);
counter.innerText = count;
this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-danger', hasMentions);
}
}

View file

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

View file

@ -0,0 +1,83 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load markdown %}
{% load humanize %}
{% block title %}{{ author.name }}{% endblock %}
{% block content %}
<div class="block">
<div class="columns is-mobile">
<div class="column">
<h1 class="title">{{ author.name }}</h1>
</div>
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
<div class="column is-narrow">
<a href="{{ author.local_path }}/edit">
<span class="icon icon-pencil" title="{% trans 'Edit Author' %}" aria-hidden="True"></span>
<span>{% trans "Edit Author" %}</span>
</a>
</div>
{% endif %}
</div>
</div>
<div class="block content columns">
{% if author.aliases or author.born or author.died or author.wikipedia_link %}
<div class="column is-narrow">
<div class="box">
<dl>
{% if author.aliases %}
<div class="is-flex">
<dt class="mr-1">{% trans "Aliases:" %}</dt>
<dd itemprop="aliases">{{ author.aliases|join:', ' }}</dd>
</div>
{% endif %}
{% if author.born %}
<div class="is-flex">
<dt class="mr-1">{% trans "Born:" %}</dt>
<dd itemprop="aliases">{{ author.born|naturalday }}</dd>
</div>
{% endif %}
{% if author.aliases %}
<div class="is-flex">
<dt class="mr-1">{% trans "Died:" %}</dt>
<dd itemprop="aliases">{{ author.died|naturalday }}</dd>
</div>
{% endif %}
</dl>
{% if author.wikipedia_link %}
<p><a href="{{ author.wikipedia_link }}" rel=”noopener” target="_blank">{% trans "Wikipedia" %}</a></p>
{% endif %}
{% if author.openlibrary_key %}
<p class="mb-0">
<a href="https://openlibrary.org/authors/{{ author.openlibrary_key }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a>
</p>
{% endif %}
{% if author.inventaire_id %}
<p class="mb-0">
<a href="https://inventaire.io/entity/{{ author.inventaire_id }}" target="_blank" rel="noopener">{% trans "View on Inventaire" %}</a>
</p>
{% endif %}
</div>
</div>
{% endif %}
<div class="column">
{% if author.bio %}
{{ author.bio|to_markdown|safe }}
{% endif %}
</div>
</div>
<div class="block">
<h3 class="title is-4">{% blocktrans with name=author.name %}Books by {{ name }}{% endblocktrans %}</h3>
<div class="columns is-multiline is-mobile">
{% for book in books %}
<div class="column is-one-fifth">
{% include 'discover/small-book.html' with book=book %}
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -29,44 +29,64 @@
<div class="columns">
<div class="column">
<h2 class="title is-4">{% trans "Metadata" %}</h2>
<p><label class="label" for="id_name">{% trans "Name:" %}</label> {{ form.name }}</p>
<p class="mb-2"><label class="label" for="id_name">{% trans "Name:" %}</label> {{ form.name }}</p>
{% for error in form.name.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_bio">{% trans "Bio:" %}</label> {{ form.bio }}</p>
<p class="mb-2">
<label class="label" for="id_aliases">{% trans "Aliases:" %}</label>
{{ form.aliases }}
<span class="help">{% trans "Separate multiple values with commas." %}</span>
</p>
{% for error in form.aliases.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2"><label class="label" for="id_bio">{% trans "Bio:" %}</label> {{ form.bio }}</p>
{% for error in form.bio.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_wikipedia_link">{% trans "Wikipedia link:" %}</label> {{ form.wikipedia_link }}</p>
<p class="mb-2"><label class="label" for="id_wikipedia_link">{% trans "Wikipedia link:" %}</label> {{ form.wikipedia_link }}</p>
{% for error in form.wikipedia_link.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_born">{% trans "Birth date:" %}</label> {{ form.born }}</p>
<p class="mb-2">
<label class="label" for="id_born">{% trans "Birth date:" %}</label>
<input type="date" name="born" value="{{ form.born.value|date:'Y-m-d' }}" class="input" id="id_born">
</p>
{% for error in form.born.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_died">{% trans "Death date:" %}</label> {{ form.died }}</p>
<p class="mb-2">
<label class="label" for="id_died">{% trans "Death date:" %}</label>
<input type="date" name="died" value="{{ form.died.value|date:'Y-m-d' }}" class="input" id="id_died">
</p>
{% for error in form.died.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="column">
<h2 class="title is-4">{% trans "Author Identifiers" %}</h2>
<p><label class="label" for="id_openlibrary_key">{% trans "Openlibrary key:" %}</label> {{ form.openlibrary_key }}</p>
<p class="mb-2"><label class="label" for="id_openlibrary_key">{% trans "Openlibrary key:" %}</label> {{ form.openlibrary_key }}</p>
{% for error in form.openlibrary_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_librarything_key">{% trans "Librarything key:" %}</label> {{ form.librarything_key }}</p>
<p class="mb-2"><label class="label" for="id_inventaire_id">{% trans "Inventaire ID:" %}</label> {{ form.inventaire_id }}</p>
{% for error in form.inventaire_id.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2"><label class="label" for="id_librarything_key">{% trans "Librarything key:" %}</label> {{ form.librarything_key }}</p>
{% for error in form.librarything_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p><label class="label" for="id_goodreads_key">{% trans "Goodreads key:" %}</label> {{ form.goodreads_key }}</p>
<p class="mb-2"><label class="label" for="id_goodreads_key">{% trans "Goodreads key:" %}</label> {{ form.goodreads_key }}</p>
{% for error in form.goodreads_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
@ -76,7 +96,7 @@
<div class="block">
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
<a class="button" href="/author/{{ author.id }}">{% trans "Cancel" %}</a>
<a class="button" href="{{ author.local_path }}">{% trans "Cancel" %}</a>
</div>
</form>

View file

@ -1,7 +1,5 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
{% load i18n %}{% load bookwyrm_tags %}{% load humanize %}{% load utilities %}
{% block title %}{{ book.title }}{% endblock %}
@ -38,9 +36,8 @@
{% if user_authenticated and can_edit_book %}
<div class="column is-narrow">
<a href="{{ book.id }}/edit">
<span class="icon icon-pencil" title="{% trans "Edit Book" %}">
<span class="is-sr-only">{% trans "Edit Book" %}</span>
</span>
<span class="icon icon-pencil" title="{% trans "Edit Book" %}" aria-hidden=True></span>
<span>{% trans "Edit Book" %}</span>
</a>
</div>
{% endif %}
@ -48,10 +45,9 @@
<div class="columns">
<div class="column is-one-fifth">
<div class="is-clipped">
{% include 'snippets/book_cover.html' with book=book size=large %}
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m-mobile' %}
{% include 'snippets/rate_action.html' with user=request.user book=book %}
</div>
<div class="mb-3">
{% include 'snippets/shelve_button/shelve_button.html' %}
</div>
@ -81,6 +77,9 @@
{% if book.openlibrary_key %}
<p><a href="https://openlibrary.org/books/{{ book.openlibrary_key }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a></p>
{% endif %}
{% if book.inventaire_id %}
<p><a href="https://inventaire.io/entity/{{ book.inventaire_id }}" target="_blank" rel="noopener">{% trans "View on Inventaire" %}</a></p>
{% endif %}
</section>
</div>
@ -153,17 +152,17 @@
</div>
{% if user_authenticated %}
<hr aria-hidden="true">
<section class="block">
<header class="columns">
<h2 class="column title is-5 mb-1">{% trans "Your reading activity" %}</h2>
<div class="column">
<h2 class="title is-5">{% trans "Your reading activity" %}</h2>
</div>
<div class="column is-narrow">
{% trans "Add read dates" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon="plus" class="is-small" controls_text="add-readthrough" %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="plus" class="is-small" controls_text="add-readthrough" focus="add-readthrough-focus" %}
</div>
</header>
{% if not readthroughs.exists %}
<p>{% trans "You don't have any reading activity for this book." %}</p>
{% endif %}
<section class="is-hidden box" id="add-readthrough">
<form name="add-readthrough" action="/create-readthrough" method="post">
{% include 'snippets/readthrough_form.html' with readthrough=None %}
@ -178,15 +177,75 @@
</div>
</form>
</section>
{% if not readthroughs.exists %}
<p>{% trans "You don't have any reading activity for this book." %}</p>
{% endif %}
{% for readthrough in readthroughs %}
{% include 'snippets/readthrough.html' with readthrough=readthrough %}
{% endfor %}
</section>
<hr aria-hidden="true">
<section class="box">
{% include 'snippets/create_status.html' with book=book hide_cover=True %}
</section>
{% endif %}
<div class="block" id="reviews">
{% if request.user.is_authenticated %}
{% 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 %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}">{% trans "Reviews" %} ({{ review_count }})</a>
</li>
{% if user_statuses.review_count %}
{% url 'book-user-statuses' book.id 'review' as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}">{% trans "Your reviews" %} ({{ user_statuses.review_count }})</a>
</li>
{% endif %}
{% if user_statuses.comment_count %}
{% url 'book-user-statuses' book.id 'comment' as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}">{% trans "Your comments" %} ({{ user_statuses.comment_count }})</a>
</li>
{% endif %}
{% if user_statuses.quotation_count %}
{% url 'book-user-statuses' book.id 'quote' as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}">{% trans "Your quotes" %} ({{ user_statuses.quotation_count }})</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endif %}
{% for status in statuses %}
<div
class="block"
{% if status.status_type == 'Review' or status.status_type == 'Rating' %}
itemprop="review"
itemscope
itemtype="https://schema.org/Review"
{% endif %}
>
{% include 'snippets/status/status.html' with status=status hide_book=True depth=1 %}
</div>
{% endfor %}
{% if ratings %}
<div class="block is-flex is-flex-wrap-wrap">
{% for rating in ratings %}
{% include 'book/rating.html' with user=rating.user rating=rating %}
{% endfor %}
</div>
{% endif %}
<div class="block">
{% include 'snippets/pagination.html' with page=statuses path=request.path anchor="#reviews" %}
</div>
</div>
</div>
<div class="column is-one-fifth">
{% if book.subjects %}
@ -245,80 +304,6 @@
</div>
</div>
<div class="block" id="reviews">
{% if request.user.is_authenticated %}
<nav class="tabs">
<ul>
{% url 'book' book.id as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}">{% trans "Reviews" %} ({{ review_count }})</a>
</li>
{% if user_statuses.review_count %}
{% url 'book-user-statuses' book.id 'review' as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}">{% trans "Your reviews" %} ({{ user_statuses.review_count }})</a>
</li>
{% endif %}
{% if user_statuses.comment_count %}
{% url 'book-user-statuses' book.id 'comment' as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}">{% trans "Your comments" %} ({{ user_statuses.comment_count }})</a>
</li>
{% endif %}
{% if user_statuses.quotation_count %}
{% url 'book-user-statuses' book.id 'quote' as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}">{% trans "Your quotes" %} ({{ user_statuses.quotation_count }})</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% for review in statuses %}
<div
class="block"
itemprop="review"
itemscope
itemtype="https://schema.org/Review"
>
{% with status=review hide_book=True depth=1 %}
{% include 'snippets/status/status.html' %}
{% endwith %}
</div>
{% endfor %}
<div class="block is-flex is-flex-wrap-wrap">
{% for rating in ratings %}
{% with user=rating.user %}
<div class="block mr-5">
<div class="media">
<div class="media-left">
{% include 'snippets/avatar.html' %}
</div>
<div class="media-content">
<div>
<a href="{{ user.local_path }}">{{ user.display_name }}</a>
</div>
<div class="is-flex">
<p class="mr-1">{% trans "rated it" %}</p>
{% include 'snippets/stars.html' with rating=rating.rating %}
</div>
<div>
<a href="{{ rating.remote_id }}">{{ rating.published_date | naturaltime }}</a>
</div>
</div>
</div>
</div>
{% endwith %}
{% endfor %}
</div>
<div class="block">
{% include 'snippets/pagination.html' with page=statuses path=request.path anchor="#reviews" %}
</div>
</div>
</div>
{% endwith %}
{% endblock %}

View file

@ -1,5 +1,4 @@
{% spaceless %}
{% load i18n %}
<dl>

View file

@ -88,6 +88,7 @@
<div class="column is-half">
<section class="block">
<h2 class="title is-4">{% trans "Metadata" %}</h2>
<p class="mb-2">
<label class="label" for="id_title">{% trans "Title:" %}</label>
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title">
@ -117,15 +118,27 @@
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2"><label class="label" for="id_series_number">{% trans "Series number:" %}</label> {{ form.series_number }} </p>
<p class="mb-2">
<label class="label" for="id_series_number">{% trans "Series number:" %}</label>
{{ form.series_number }}
</p>
{% for error in form.series_number.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2">
<label class="label" for="id_languages">{% trans "Languages:" %}</label>
{{ form.languages }}
<span class="help">{% trans "Separate multiple values with commas." %}</span>
</p>
{% for error in form.languages.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2">
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
{{ form.publishers }}
<span class="help">{% trans "Separate multiple publishers with commas." %}</span>
<span class="help">{% trans "Separate multiple values with commas." %}</span>
</p>
{% for error in form.publishers.errors %}
<p class="help is-danger">{{ error | escape }}</p>
@ -162,17 +175,18 @@
{% endif %}
<label class="label" for="id_add_author">{% trans "Add Authors:" %}</label>
<input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'John Doe, Jane Smith' %}" value="{{ add_author }}" {% if confirm_mode %}readonly{% endif %}>
<p class="help">Separate multiple author names with commas.</p>
<span class="help">{% trans "Separate multiple values with commas." %}</span>
</section>
</div>
<div class="column is-half">
<h2 class="title is-4">{% trans "Cover" %}</h2>
<div class="columns">
<div class="column is-narrow">
{% include 'snippets/book_cover.html' with book=book size="small" %}
<div class="column is-3 is-cover">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-xl-mobile is-w-auto-tablet' %}
</div>
<div class="column is-narrow">
<div class="column">
<div class="block">
<p>
<label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
@ -215,18 +229,27 @@
{% for error in form.isbn_13.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2"><label class="label" for="id_isbn_10">{% trans "ISBN 10:" %}</label> {{ form.isbn_10 }} </p>
{% for error in form.isbn_10.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2"><label class="label" for="id_openlibrary_key">{% trans "Openlibrary key:" %}</label> {{ form.openlibrary_key }} </p>
<p class="mb-2"><label class="label" for="id_openlibrary_key">{% trans "Openlibrary ID:" %}</label> {{ form.openlibrary_key }} </p>
{% for error in form.openlibrary_key.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2"><label class="label" for="id_inventaire_id">{% trans "Inventaire ID:" %}</label> {{ form.inventaire_id }} </p>
{% for error in form.inventaire_id.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2"><label class="label" for="id_oclc_number">{% trans "OCLC Number:" %}</label> {{ form.oclc_number }} </p>
{% for error in form.oclc_number.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2"><label class="label" for="id_asin">{% trans "ASIN:" %}</label> {{ form.asin }} </p>
{% for error in form.ASIN.errors %}
<p class="help is-danger">{{ error | escape }}</p>
@ -238,7 +261,7 @@
{% if not confirm_mode %}
<div class="block">
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
<a class="button" href="/book/{{ book.id }}">{% trans "Cancel" %}</a>
<a class="button" href="{{ book.local_path}}">{% trans "Cancel" %}</a>
</div>
{% endif %}
</form>

View file

@ -1,44 +1,45 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load i18n %}{% load utilities %}
{% block title %}{% blocktrans with book_title=work.title %}Editions of {{ book_title }}{% endblocktrans %}{% endblock %}
{% block title %}{% blocktrans with book_title=work|book_title %}Editions of {{ book_title }}{% endblocktrans %}{% endblock %}
{% block content %}
<div class="block">
<h1 class="title">{% blocktrans with work_path=work.local_path work_title=work.title %}Editions of <a href="{{ work_path }}">"{{ work_title }}"</a>{% endblocktrans %}</h1>
<h1 class="title">{% blocktrans with work_path=work.local_path work_title=work|book_title %}Editions of <a href="{{ work_path }}">"{{ work_title }}"</a>{% endblocktrans %}</h1>
</div>
{% include 'book/edition_filters.html' %}
<div class="block">
{% for book in editions %}
<div class="columns">
<div class="column is-2">
<a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book size="medium" %}
<div class="columns is-gapless mb-6">
<div class="column is-cover">
<a href="{{ book.local_path }}">
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-m is-h-m align to-l-mobile' %}
</a>
</div>
<div class="column is-7">
<h2 class="title is-5">
<a href="/book/{{ book.id }}" class="has-text-black">
{{ book.title }}
<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">
{{ book|book_title }}
</a>
</h2>
{% with book=book %}
<div class="columns is-multiline">
<div class="columns is-multiline is-gapless ml-3-tablet">
<div class="column is-half">
{% include 'book/publisher_info.html' %}
</div>
<div class="column is-half ">
<div class="column ml-3-tablet">
{% include 'book/book_identifiers.html' %}
</div>
</div>
{% endwith %}
</div>
<div class="column is-3">
<div class="column is-narrow">
{% include 'snippets/shelve_button/shelve_button.html' with book=book switch_mode=True %}
</div>
</div>

View file

@ -0,0 +1,22 @@
{% load i18n %}{% load status_display %}
<div class="block mr-5">
<div class="media">
<div class="media-left">
{% include 'snippets/avatar.html' with user=user %}
</div>
<div class="media-content">
<div>
<a href="{{ user.local_path }}">{{ user.display_name }}</a>
</div>
<div class="is-flex">
<p class="mr-1">{% trans "rated it" %}</p>
{% include 'snippets/stars.html' with rating=rating.rating %}
</div>
<div>
<a href="{{ rating.remote_id }}">{{ rating.published_date|published_date }}</a>
</div>
</div>
</div>
</div>

View file

@ -1,5 +1,5 @@
{% spaceless %}
{% load bookwyrm_tags %}
{% load utilities %}
{% with 0|uuid as uuid %}
<div

View file

@ -1,6 +1,6 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load utilities %}
{% block title %}{% trans "Compose status" %}{% endblock %}
{% block content %}
@ -11,14 +11,15 @@
{% with 0|uuid as uuid %}
<div class="box columns">
{% if book %}
<div class="column is-one-third">
<div class="column is-3 is-cover">
<div class="block">
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto-tablet is-h-l-mobile' %}</a>
</div>
<h3 class="title is-6">{% include 'snippets/book_titleby.html' with book=book %}</h3>
</div>
{% endif %}
<div class="column is-two-thirds">
<div class="column">
{% if draft.reply_parent %}
{% include 'snippets/status/status.html' with status=draft.reply_parent no_interact=True %}
{% endif %}

View file

@ -1,7 +1,5 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
{% block title %}{% trans "Directory" %}{% endblock %}
@ -41,59 +39,7 @@
<div class="columns is-multiline">
{% for user in users %}
<div class="column is-one-third">
<div class="card is-stretchable">
<div class="card-content">
<div class="media">
<a href="{{ user.local_path }}" class="media-left">
{% include 'snippets/avatar.html' with user=user large=True %}
</a>
<div class="media-content">
<a href="{{ user.local_path }}" class="is-block mb-2">
<span class="title is-4 is-block">{{ user.display_name }}</span>
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
</a>
{% include 'snippets/follow_button.html' with user=user %}
</div>
</div>
<div>
{% if user.summary %}
{{ user.summary | to_markdown | safe | truncatechars_html:40 }}
{% else %}&nbsp;{% endif %}
</div>
</div>
<footer class="card-footer">
{% if user != request.user %}
{% if user.mutuals %}
<div class="card-footer-item">
<div class="has-text-centered">
<p class="title is-6 mb-0">{{ user.mutuals }}</p>
<p class="help">{% blocktrans count counter=user.mutuals %}follower you follow{% plural %}followers you follow{% endblocktrans %}</p>
</div>
</div>
{% elif user.shared_books %}
<div class="card-footer-item">
<div class="has-text-centered">
<p class="title is-6 mb-0">{{ user.shared_books }}</p>
<p class="help">{% blocktrans count counter=user.shared_books %}book on your shelves{% plural %}books on your shelves{% endblocktrans %}</p>
</div>
</div>
{% endif %}
{% endif %}
<div class="card-footer-item">
<div class="has-text-centered">
<p class="title is-6 mb-0">{{ user.status_set.count|intword }}</p>
<p class="help">{% trans "posts" %}</p>
</div>
</div>
<div class="card-footer-item">
<div class="has-text-centered">
<p class="title is-6 mb-0">{{ user.last_active_date|naturalday }}</p>
<p class="help">{% trans "last active" %}</p>
</div>
</div>
</footer>
</div>
{% include 'directory/user_card.html' %}
</div>
{% endfor %}
</div>
@ -103,7 +49,3 @@
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/localstorage.js"></script>
{% endblock %}

View file

@ -0,0 +1,58 @@
{% load i18n %}
{% load utilities %}
{% load markdown %}
{% load humanize %}
<div class="card is-stretchable">
<div class="card-content">
<div class="media">
<a href="{{ user.local_path }}" class="media-left">
{% include 'snippets/avatar.html' with user=user large=True %}
</a>
<div class="media-content">
<a href="{{ user.local_path }}" class="is-block mb-2">
<span class="title is-4 is-block">{{ user.display_name }}</span>
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
</a>
{% include 'snippets/follow_button.html' with user=user %}
</div>
</div>
<div>
{% if user.summary %}
{{ user.summary|to_markdown|safe|truncatechars_html:40 }}
{% else %}&nbsp;{% endif %}
</div>
</div>
<footer class="card-footer">
{% if user != request.user %}
{% if user.mutuals %}
<div class="card-footer-item">
<div class="has-text-centered">
<p class="title is-6 mb-0">{{ user.mutuals }}</p>
<p class="help">{% blocktrans count counter=user.mutuals %}follower you follow{% plural %}followers you follow{% endblocktrans %}</p>
</div>
</div>
{% elif user.shared_books %}
<div class="card-footer-item">
<div class="has-text-centered">
<p class="title is-6 mb-0">{{ user.shared_books }}</p>
<p class="help">{% blocktrans count counter=user.shared_books %}book on your shelves{% plural %}books on your shelves{% endblocktrans %}</p>
</div>
</div>
{% endif %}
{% endif %}
<div class="card-footer-item">
<div class="has-text-centered">
<p class="title is-6 mb-0">{{ user.status_set.count|intword }}</p>
<p class="help">{% trans "posts" %}</p>
</div>
</div>
<div class="card-footer-item">
<div class="has-text-centered">
<p class="title is-6 mb-0">{{ user.last_active_date|naturalday }}</p>
<p class="help">{% trans "last active" %}</p>
</div>
</div>
</footer>
</div>

View file

@ -1,6 +1,6 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load markdown %}
{% block title %}{% trans "Welcome" %}{% endblock %}

View file

@ -1,19 +1,38 @@
{% load bookwyrm_tags %}
{% load markdown %}
{% load i18n %}
{% if book %}
<div class="columns">
<div class="column is-narrow">
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="large" %}</a>
{% with book=book %}
<div class="columns is-gapless">
<div class="column is-5-tablet is-cover">
<a
class="align to-b to-l"
href="{{ book.local_path }}"
>{% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-w-auto-tablet' %}</a>
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
</div>
<div class="column">
<h3 class="title is-5"><a href="/book/{{ book.id }}">{{ book.title }}</a></h3>
<div class="column mt-3-mobile ml-3-tablet">
<h3 class="title is-5">
<a href="{{ book.local_path }}">{{ book.title }}</a>
</h3>
{% if book.authors %}
<p class="subtitle is-5">{% trans "by" %} {% include 'snippets/authors.html' with book=book %}</p>
<p class="subtitle is-5">
{% trans "by" %}
{% include 'snippets/authors.html' %}
</p>
{% endif %}
{% if book|book_description %}
<blockquote class="content">{{ book|book_description|to_markdown|safe|truncatewords_html:50 }}</blockquote>
<blockquote class="content">
{{ book|book_description|to_markdown|safe|truncatewords_html:50 }}
</blockquote>
{% endif %}
</div>
</div>
{% endwith %}
{% endif %}

View file

@ -1,12 +1,24 @@
{% load bookwyrm_tags %}
{% load i18n %}
{% if book %}
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
{% with book=book %}
<a href="{{ book.local_path }}">
{% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-h-l-tablet is-w-auto align to-b to-l' %}
</a>
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
<h3 class="title is-6"><a href="/book/{{ book.id }}">{{ book.title }}</a></h3>
{% if book.authors %}
<p class="subtitle is-6">{% trans "by" %} {% include 'snippets/authors.html' with book=book %}</p>
{% endif %}
<h3 class="title is-6">
<a href="{{ book.local_path }}">{{ book.title }}</a>
</h3>
{% if book.authors %}
<p class="subtitle is-6">
{% trans "by" %}
{% include 'snippets/authors.html' %}
</p>
{% endif %}
{% endwith %}
{% endif %}

View file

@ -10,11 +10,11 @@
{% trans "Direct Messages" %}
{% endif %}
</h1>
{% if partner %}<p class="subtitle"><a href="/direct-messages"><span class="icon icon-arrow-left" aria-hidden="true"></span> {% trans "All messages" %}</a></p>{% endif %}
{% if partner %}<p class="subtitle"><a href="{% url 'direct-messages' %}"><span class="icon icon-arrow-left" aria-hidden="true"></span> {% trans "All messages" %}</a></p>{% endif %}
</header>
<div class="box">
{% include 'snippets/create_status_form.html' with type="direct" uuid=1 mentions=partner %}
{% include 'snippets/create_status_form.html' with type="direct" uuid=1 mention=partner %}
</div>
<section class="block">

View file

@ -1,7 +1,6 @@
{% extends 'feed/feed_layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
{% block panel %}
<h1 class="title">

View file

@ -1,6 +1,5 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block title %}{% trans "Updates" %}{% endblock %}
@ -37,7 +36,7 @@
aria-label="{{ book.title }}"
aria-selected="{% if active_book == book.id|stringformat:'d' %}true{% elif shelf_counter == 1 and forloop.first %}true{% else %}false{% endif %}"
aria-controls="book-{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book size="medium" %}
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' %}
</a>
</li>
{% endfor %}

View file

@ -1,5 +1,5 @@
{% load i18n %}
{% load bookwyrm_tags %}
{% load utilities %}
{% load humanize %}
<div class="columns is-mobile scroll-x mb-0">
{% for user in suggested_users %}

View file

@ -1,4 +1,4 @@
{% load bookwyrm_tags %}
{% load status_display %}
<div class="block">
{% with depth=depth|add:1 %}

View file

@ -1,8 +1,8 @@
{% load i18n %}
<div class="column is-narrow is-clipped has-text-centered">
{% include 'snippets/book_cover.html' with book=book %}
<label class="label" for="id_shelve_{{ book.id }}">
<div class="select is-small">
<div class="column is-cover">
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-l is-h-m-mobile' %}
<div class="select is-small mt-1 mb-3">
<select name="{{ book.id }}" aria-label="{% blocktrans with book_title=book.title %}Have you read {{ book_title }}?{% endblocktrans %}">
<option disabled selected value>Add to your books</option>
{% for shelf in request.user.shelf_set.all %}

View file

@ -23,11 +23,15 @@
<form class="block" name="add-books" method="post" action="{% url 'get-started-books' %}">
{% csrf_token %}
<h3 class="title is-5">{% trans "Suggested Books" %}</h3>
<fieldset name="books" class="columns scroll-x is-mobile">
<div class="block scroll-x">
<fieldset name="books" class="columns is-mobile">
{% if book_results %}
<div class="column is-narrow content">
<div class="column is-narrow">
<p class="help mb-0">Search results</p>
<div class="columns is-mobile">
{% for book in book_results %}
{% include 'get_started/book_preview.html' %}
@ -35,11 +39,13 @@
</div>
</div>
{% endif %}
{% if popular_books %}
<div class="column is-narrow content">
<div class="column is-narrow">
<p class="help mb-0">
{% blocktrans %}Popular on {{ site_name }}{% endblocktrans %}
</p>
<div class="columns is-mobile">
{% for book in popular_books %}
{% include 'get_started/book_preview.html' %}
@ -47,10 +53,13 @@
</div>
</div>
{% endif %}
{% if not book_results and not popular_books %}
<p><em>{% trans "No books found" %}</em></p>
{% endif %}
</fieldset>
</div>
<button type="submit" class="button is-primary">{% trans "Save &amp; continue" %}</button>
</form>
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'user/user_layout.html' %}
{% extends 'user/layout.html' %}
{% load i18n %}
{% block header %}
@ -9,7 +9,7 @@
{% if is_self and goal %}
<div class="column is-narrow">
{% trans "Edit Goal" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="show-edit-goal" focus="edit-form-header" %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="show-edit-goal" focus="edit-form-header" %}
</div>
{% endif %}
</div>
@ -45,20 +45,21 @@
</section>
{% if goal.books %}
<section class="content">
<h2>
<section>
<h2 class="title is-4">
{% if goal.user == request.user %}
{% blocktrans %}Your {{ year }} Books{% endblocktrans %}
{% else %}
{% blocktrans with username=goal.user.display_name %}{{ username }}'s {{ year }} Books{% endblocktrans %}
{% endif %}
</h2>
<div class="columns is-multiline">
<div class="columns is-mobile is-multiline">
{% for book in goal.books %}
<div class="column is-one-fifth">
<div class="is-clipped">
<a href="{{ book.book.local_path }}">{% include 'snippets/book_cover.html' with book=book.book %}</a>
</div>
<div class="column is-cover">
<a href="{{ book.book.local_path }}">
{% include 'snippets/book_cover.html' with book=book.book cover_class='is-h-xl is-h-l-mobile' %}
</a>
</div>
{% endfor %}
</div>

View file

@ -20,6 +20,9 @@
<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>
@ -56,7 +59,7 @@
{% endif %}
<ul>
{% for job in jobs %}
<li><a href="/import/{{ job.id }}">{{ job.created_date | naturaltime }}</a></li>
<li><a href="{% url 'import-status' job.id %}">{{ job.created_date | naturaltime }}</a></li>
{% endfor %}
</ul>
</div>

View file

@ -1,6 +1,5 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
{% block title %}{% trans "Import Status" %}{% endblock %}
@ -54,8 +53,8 @@
<input class="checkbox" type="checkbox" name="import_item" value="{{ item.id }}" id="import-item-{{ item.id }}">
<label for="import-item-{{ item.id }}">
Line {{ item.index }}:
<strong>{{ item.data|dict_key:'Title' }}</strong> by
{{ item.data|dict_key:'Author' }}
<strong>{{ item.data.Title }}</strong> by
{{ item.data.Author }}
</label>
<p>
{{ item.fail_reason }}.
@ -90,8 +89,8 @@
<li class="pb-1">
<p>
Line {{ item.index }}:
<strong>{{ item.data|dict_key:'Title' }}</strong> by
{{ item.data|dict_key:'Author' }}
<strong>{{ item.data.Title }}</strong> by
{{ item.data.Author }}
</p>
<p>
{{ item.fail_reason }}.
@ -124,16 +123,16 @@
<tr>
<td>
{% if item.book %}
<a href="/book/{{ item.book.id }}">
{% include 'snippets/book_cover.html' with book=item.book size='small' %}
<a href="{{ item.book.local_path }}">
{% include 'snippets/book_cover.html' with book=item.book cover_class='is-h-s' %}
</a>
{% endif %}
</td>
<td>
{{ item.data|dict_key:'Title' }}
{{ item.data.Title }}
</td>
<td>
{{ item.data|dict_key:'Author' }}
{{ item.data.Author }}
</td>
<td>
{% if item.book %}

View file

@ -1,5 +1,4 @@
{% load bookwyrm_tags %}
{% load i18n %}
{% load layout %}{% load i18n %}
<!DOCTYPE html>
<html lang="{% get_lang %}">
<head>
@ -94,12 +93,12 @@
</a>
</li>
<li>
<a href="/import" class="navbar-item">
<a href="{% url 'import' %}" class="navbar-item">
{% trans 'Import Books' %}
</a>
</li>
<li>
<a href="/preferences/profile" class="navbar-item">
<a href="{% url 'prefs-profile' %}" class="navbar-item">
{% trans 'Settings' %}
</a>
</li>
@ -122,21 +121,24 @@
{% endif %}
<li class="navbar-divider" role="presentation"></li>
<li>
<a href="/logout" class="navbar-item">
<a href="{% url 'logout' %}" class="navbar-item">
{% trans 'Log out' %}
</a>
</li>
</ul>
</div>
<div class="navbar-item">
<a href="/notifications" class="tags has-addons">
<a href="{% url 'notifications' %}" class="tags has-addons">
<span class="tag is-medium">
<span class="icon icon-bell" title="{% trans 'Notifications' %}">
<span class="is-sr-only">{% trans "Notifications" %}</span>
</span>
</span>
<span class="{% if not request.user|notification_count %}is-hidden {% endif %}tag is-danger is-medium transition-x" data-poll-wrapper>
<span data-poll="notifications">{{ request.user | notification_count }}</span>
<span
class="{% if not request.user.unread_notification_count %}is-hidden {% elif request.user.has_unread_mentions %}is-danger {% endif %}tag is-medium transition-x"
data-poll-wrapper
>
<span data-poll="notifications">{{ request.user.unread_notification_count }}</span>
</span>
</a>
</div>
@ -155,7 +157,7 @@
<div class="column">
<label class="is-sr-only" for="id_password">{% trans "Username:" %}</label>
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password" placeholder="{% trans 'password' %}">
<p class="help"><a href="/password-reset">{% trans "Forgot your password?" %}</a></p>
<p class="help"><a href="{% url 'password-reset' %}">{% trans "Forgot your password?" %}</a></p>
</div>
<div class="column is-narrow">
<button class="button is-primary" type="submit">{% trans "Log in" %}</button>
@ -179,6 +181,15 @@
</div>
</nav>
{% if request.user.is_authenticated and active_announcements.exists %}
<div class="block is-flex-grow-1">
<div class="container">
{% for announcement in active_announcements %}
{% include 'snippets/announcement.html' with announcement=announcement %}
{% endfor %}
</div>
</div>
{% endif %}
<div class="section is-flex-grow-1">
<div class="container">
@ -190,25 +201,35 @@
<footer class="footer">
<div class="container">
<div class="columns">
<div class="column">
<div class="column is-one-fifth">
<p>
<a href="/about">{% trans "About this server" %}</a>
<a href="{% url 'about' %}">{% trans "About this server" %}</a>
</p>
{% if site.admin_email %}
<p>
<a href="mailto:{{ site.admin_email }}">{% trans "Contact site admin" %}</a>
</p>
{% endif %}
<p>
<a href="https://docs.joinbookwyrm.com/">{% trans "Documentation" %}</a>
</p>
</div>
<div class="column content is-two-fifth">
{% if site.support_link %}
<div class="column">
<p>
<span class="icon icon-heart"></span>
{% blocktrans with site_name=site.name support_link=site.support_link support_title=site.support_title %}Support {{ site_name }} on <a href="{{ support_link }}" target="_blank">{{ support_title }}</a>{% endblocktrans %}
</p>
{% endif %}
<p>
{% blocktrans %}BookWyrm's source code is freely available. You can contribute or report issues on <a href="https://github.com/mouse-reeve/bookwyrm">GitHub</a>.{% endblocktrans %}
</p>
</div>
{% if site.footer_item %}
<div class="column">
<p>{{ site.footer_item|safe }}</p>
</div>
{% endif %}
<div class="column">
{% trans 'BookWyrm is open source software. You can contribute or report issues on <a href="https://github.com/mouse-reeve/bookwyrm">GitHub</a>.' %}
</div>
</div>
</div>
</footer>
@ -217,6 +238,7 @@
var csrf_token = '{{ csrf_token }}';
</script>
<script src="/static/js/bookwyrm.js"></script>
<script src="/static/js/localstorage.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View file

@ -2,32 +2,54 @@
{% load i18n %}
{% block panel %}
<section class="content block">
<h2>{% trans "Pending Books" %}</h2>
<p><a href="{% url 'list' list.id %}">{% trans "Go to list" %}</a></p>
<section class="block">
<div class="columns is-mobile is-multiline is-align-items-baseline">
<div class="column is-narrow">
<h2 class="title is-4">{% trans "Pending Books" %}</h2>
</div>
<p class="column is-narrow"><a href="{% url 'list' list.id %}">{% trans "Go to list" %}</a></p>
</div>
{% if not pending.exists %}
<p>{% trans "You're all set!" %}</p>
{% else %}
<table class="table is-striped">
<tr>
<th></th>
<th>{% trans "Book" %}</th>
<th>{% trans "Suggested by" %}</th>
<th></th>
</tr>
<dl>
{% for item in pending %}
<tr>
<td>
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=item.book size="small" %}</a>
</td>
<td>
{% include 'snippets/book_titleby.html' with book=item.book %}
</td>
<td>
<a href="{{ item.user.local_path }}">{{ item.user.display_name }}</a>
</td>
<td>
<div class="field has-addons">
{% with book=item.book %}
<div
class="
columns is-gapless
is-vcentered is-justify-content-space-between
mb-6
"
>
<dt class="column mr-auto">
<div class="columns is-mobile is-gapless is-vcentered">
<a
class="column is-cover"
href="{{ book.local_path }}"
aria-hidden="true"
>
{% include 'snippets/book_cover.html' with cover_class='is-w-xs-mobile is-w-s is-h-xs-mobile is-h-s' %}
</a>
<div class="column ml-3">
{% include 'snippets/book_titleby.html' %}
</div>
</div>
</dt>
<dd class="column is-4-tablet mx-3-tablet my-3-mobile">
{% trans "Suggested by" %}
<a href="{{ item.user.local_path }}">
{{ item.user.display_name }}
</a>
</dd>
<dd class="column is-narrow field has-addons">
<form class="control" method="POST" action="{% url 'list-curate' list.id %}">
{% csrf_token %}
<input type="hidden" name="item" value="{{ item.id }}">
@ -39,12 +61,12 @@
<input type="hidden" name="item" value="{{ item.id }}">
<input type="hidden" name="approved" value="false">
<button class="button is-danger is-light">{% trans "Discard" %}</button>
</div>
</form>
</td>
</tr>
</dd>
</div>
{% endwith %}
{% endfor %}
</table>
</dl>
{% endif %}
</section>
{% endblock %}

View file

@ -1,6 +1,7 @@
{% extends 'lists/list_layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load markdown %}
{% block panel %}
{% if request.user == list.user and pending_count %}
@ -13,23 +14,51 @@
<div class="columns mt-3">
<section class="column is-three-quarters">
{% if request.GET.updated %}
<div class="notification is-primary">
{% if list.curation != "open" and request.user != list.user %}
{% trans "You successfully suggested a book for this list!" %}
{% else %}
{% trans "You successfully added a book to this list!" %}
{% endif %}
</div>
{% endif %}
{% if not items.object_list.exists %}
<p>{% trans "This list is currently empty" %}</p>
{% else %}
<ol start="{{ items.start_index }}">
{% for item in items %}
<li class="block pb-3">
<li class="block mb-5">
<div class="card">
<div class="card-content columns p-0 pr-2 mb-0 is-mobile">
<div class="column is-narrow pt-0 pb-0">
<a href="{{ item.book.local_path }}">{% include 'snippets/book_cover.html' with book=item.book size="medium" %}</a>
{% with book=item.book %}
<div
class="
card-content p-0 mb-0
columns is-mobile is-gapless
"
>
<div class="column is-2-mobile is-cover align to-t">
<a href="{{ item.book.local_path }}" aria-hidden="true">
{% include 'snippets/book_cover.html' with cover_class='is-w-auto is-h-m-tablet' %}
</a>
</div>
<div class="column is-flex-direction-column is-align-items-self-start">
<span>{% include 'snippets/book_titleby.html' with book=item.book %}</span>
<div class="column ml-3">
<p>
{% include 'snippets/book_titleby.html' %}
</p>
<p>
{% include 'snippets/stars.html' with rating=item.book|rating:request.user %}
{% include 'snippets/shelve_button/shelve_button.html' with book=item.book %}
</p>
<div>
{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}
</div>
{% include 'snippets/shelve_button/shelve_button.html' %}
</div>
</div>
{% endwith %}
<div class="card-footer has-background-white-bis is-align-items-baseline">
<div class="card-footer-item">
<div>
@ -66,7 +95,7 @@
{% include "snippets/pagination.html" with page=items %}
</section>
<section class="column is-one-quarter content">
<section class="column is-one-quarter">
<h2>{% trans "Sort List" %}</h2>
<form name="sort" action="{% url 'list' list.id %}" method="GET" class="block">
<label class="label" for="id_sort_by">{% trans "Sort By" %}</label>
@ -108,15 +137,27 @@
<p>{% trans "No books found" %}</p>
{% endif %}
{% endif %}
{% if suggested_books|length > 0 %}
{% for book in suggested_books %}
{% if book %}
<div class="block columns is-mobile">
<div class="column is-narrow">
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a>
</div>
<div class="column">
<div class="columns is-mobile is-gapless">
<a
class="column is-2-mobile is-3-tablet is-cover align to-c"
href="{{ book.local_path }}"
aria-hidden="true"
>
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto is-h-s-mobile align to-t' %}
</a>
<div class="column ml-3">
<p>{% include 'snippets/book_titleby.html' with book=book %}</p>
<form name="add-book" method="post" action="{% url 'list-add-book' %}">
<form
class="mt-1"
name="add-book"
method="post"
action="{% url 'list-add-book' %}{% if query %}?q={{ query }}{% endif %}"
>
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="list" value="{{ list.id }}">
@ -124,9 +165,9 @@
</form>
</div>
</div>
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
</section>
</div>
{% endblock %}

View file

@ -1,4 +1,4 @@
{% load bookwyrm_tags %}
{% load markdown %}
<div class="columns is-multiline">
{% for list in lists %}
<div class="column is-one-quarter">
@ -8,13 +8,21 @@
<a href="{{ list.local_path }}">{{ list.name }}</a> <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>
</h4>
</header>
<div class="card-image is-flex is-clipped">
{% for book in list.listitem_set.all|slice:5 %}
<a href="{{ book.book.local_path }}">{% include 'snippets/book_cover.html' with book=book.book size="small" %}</a>
{% with list_books=list.listitem_set.all|slice:5 %}
{% if list_books %}
<div class="card-image columns is-mobile is-gapless is-clipped">
{% for book in list_books %}
<a class="column is-cover" href="{{ book.book.local_path }}">
{% include 'snippets/book_cover.html' with book=book.book cover_class='is-h-s' aria='show' %}
</a>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div class="card-content is-flex-grow-0">
<div {% if list.description %}title="{{ list.description }}"{% endif %}>
<div class="is-clipped" {% if list.description %}title="{{ list.description }}"{% endif %}>
{% if list.description %}
{{ list.description|to_markdown|safe|truncatechars_html:30 }}
{% else %}

View file

@ -1,6 +1,5 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% block title %}{{ list.name }}{% endblock %}
@ -16,7 +15,7 @@
{% if request.user == list.user %}
<div class="column is-narrow">
{% trans "Edit List" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="edit-list" focus="edit-list-header" %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit-list" focus="edit-list-header" %}
</div>
{% endif %}
</header>

View file

@ -1,5 +1,5 @@
{% extends 'layout.html' %}
{% load bookwyrm_tags %}
{% load utilities %}
{% load i18n %}
{% block title %}{% trans "Lists" %}{% endblock %}
@ -18,7 +18,7 @@
{% if request.user.is_authenticated %}
<div class="column is-narrow">
{% trans "Create List" as button_text %}
{% include 'snippets/toggle/open_button.html' with controls_text="create-list" icon="plus" text=button_text focus="create-list-header" %}
{% include 'snippets/toggle/open_button.html' with controls_text="create-list" icon_with_text="plus" text=button_text focus="create-list-header" %}
</div>
{% endif %}
</header>
@ -28,7 +28,7 @@
</div>
{% if lists %}
<section class="block content">
<section class="block">
{% include 'lists/list_items.html' with lists=lists %}
</section>

View file

@ -33,11 +33,14 @@
<button class="button is-primary" type="submit">{% trans "Log in" %}</button>
</div>
<div class="control">
<small><a href="/password-reset">{% trans "Forgot your password?" %}</a></small>
<small><a href="{% url 'password-reset' %}">{% trans "Forgot your password?" %}</a></small>
</div>
</div>
</form>
</div>
</div>
<div class="column">
<div class="box has-background-primary-light">
{% if site.allow_registration %}
<h2 class="title">{% trans "Create an Account" %}</h2>
@ -50,15 +53,15 @@
{% endif %}
</div>
</div>
</div>
<div class="column">
<div class="block">
<div class="box">
{% include 'snippets/about.html' %}
<p class="block">
<a href="/about/">{% trans "More about this site" %}</a>
<a href="{% url 'about' %}">{% trans "More about this site" %}</a>
</p>
</div>
</div>
</div>
{% endblock %}

View file

@ -1,6 +1,5 @@
{% extends 'settings/admin_layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
{% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}

View file

@ -1,18 +1,35 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load humanize %}
{% load bookwyrm_tags %}
{% load humanize %}
{% block title %}{% trans "Notifications" %}{% endblock %}
{% block content %}
<div class="block">
<header class="columns">
<div class="column">
<h1 class="title">{% trans "Notifications" %}</h1>
</div>
<form name="clear" action="/notifications" method="POST">
<form name="clear" action="/notifications" method="POST" class="column is-narrow">
{% csrf_token %}
<button class="button is-danger is-light" type="submit" class="secondary">{% trans "Delete notifications" %}</button>
</form>
</header>
<div class="block">
<nav class="tabs">
<ul>
{% url 'notifications' as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}">{% trans "All" %}</a>
</li>
{% url 'notifications' 'mentions' as tab_url %}
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
<a href="{{ tab_url }}">{% trans "Mentions" %}</a>
</li>
</ul>
</nav>
</div>
<div class="block">
@ -107,7 +124,8 @@
{% endif %}
{% endif %}
{% elif notification.related_import %}
{% blocktrans with related_id=notification.related_import.id %}Your <a href="/import/{{ related_id }}">import</a> completed.{% endblocktrans %}
{% url 'import-status' notification.related_import.id as url %}
{% blocktrans %}Your <a href="{{ url }}">import</a> completed.{% endblocktrans %}
{% elif notification.related_report %}
{% url 'settings-report' notification.related_report.id as path %}
{% blocktrans with related_id=path %}A new <a href="{{ path }}">report</a> needs moderation.{% endblocktrans %}

View file

@ -8,7 +8,7 @@
{% endblock %}
{% block panel %}
<form name="edit-profile" action="/change-password/" method="post" enctype="multipart/form-data">
<form name="edit-profile" action="{% url 'prefs-password' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="block">
<label class="label" for="id_password">{% trans "New password:" %}</label>

View file

@ -11,16 +11,19 @@
<h2 class="menu-label">{% trans "Account" %}</h2>
<ul class="menu-list">
<li>
<a href="/preferences/profile"{% if '/preferences/profile' in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Profile" %}</a>
{% url 'prefs-profile' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Profile" %}</a>
</li>
<li>
<a href="/preferences/password"{% if '/preferences/password' in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Change Password" %}</a>
{% url 'prefs-password' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Change Password" %}</a>
</li>
</ul>
<h2 class="menu-label">{% trans "Relationships" %}</h2>
<ul class="menu-list">
<li>
<a href="/preferences/block"{% if '/preferences/block' in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Blocked Users" %}</a>
{% url 'prefs-block' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Blocked Users" %}</a>
</li>
</ul>
</nav>

View file

@ -0,0 +1,78 @@
{% extends 'search/layout.html' %}
{% load i18n %}
{% block panel %}
{% if results %}
{% with results|first as local_results %}
<ul class="block">
{% for result in local_results.results %}
<li class="pd-4 mb-5">
{% include 'snippets/search_result_text.html' with result=result %}
</li>
{% endfor %}
</ul>
{% endwith %}
<div class="block">
{% for result_set in results|slice:"1:" %}
{% if result_set.results %}
<section class="box has-background-white-bis">
{% if not result_set.connector.local %}
<header class="columns is-mobile">
<div class="column">
<h3 class="title is-5">
Results from
<a href="{{ result_set.connector.base_url }}" target="_blank">{{ result_set.connector.name|default:result_set.connector.identifier }}</a>
</h3>
</div>
<div class="column is-narrow">
{% trans "Open" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text controls_text="more-results-panel" controls_uid=result_set.connector.identifier class="is-small" icon_with_text="arrow-down" pressed=forloop.first %}
{% trans "Close" as button_text %}
{% include 'snippets/toggle/close_button.html' with text=button_text controls_text="more-results-panel" controls_uid=result_set.connector.identifier class="is-small" icon_with_text="arrow-up" pressed=forloop.first %}
</div>
</header>
{% endif %}
<div class="box has-background-white is-shadowless{% if not forloop.first %} is-hidden{% endif %}" id="more-results-panel-{{ result_set.connector.identifier }}">
<div class="is-flex is-flex-direction-row-reverse">
<div>
</div>
<ul class="is-flex-grow-1">
{% for result in result_set.results %}
<li class="mb-5">
{% include 'snippets/search_result_text.html' with result=result remote_result=True %}
</li>
{% endfor %}
</ul>
</div>
</div>
</section>
{% endif %}
{% endfor %}
</div>
{% endif %}
<p class="block">
{% if request.user.is_authenticated %}
{% if not remote %}
<a href="{{ request.path }}?q={{ query }}&type=book&remote=true">
{% trans "Load results from other catalogues" %}
</a>
{% else %}
<a href="{% url 'create-book' %}">
{% trans "Manually add book" %}
</a>
{% endif %}
{% else %}
<a href="{% url 'login' %}">
{% trans "Log in to import or add books." %}
</a>
{% endif %}
</p>
{% endblock %}

View file

@ -0,0 +1,70 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block title %}{% trans "Search" %}{% endblock %}
{% block content %}
<div class="block">
<h1 class="title">
{% blocktrans %}Search{% endblocktrans %}
</h1>
</div>
<form class="block" action="{% url 'search' %}" method="GET">
<div class="field has-addons">
<div class="control">
<input type="input" class="input" name="q" value="{{ query }}" aria-label="{% trans 'Search query' %}">
</div>
<div class="control">
<div class="select" aria-label="{% trans 'Search type' %}">
<select name="type">
<option value="book" {% if type == "book" %}selected{% endif %}>{% trans "Books" %}</option>
{% if request.user.is_authenticated %}
<option value="user" {% if type == "user" %}selected{% endif %}>{% trans "Users" %}</option>
{% endif %}
<option value="list" {% if type == "list" %}selected{% endif %}>{% trans "Lists" %}</option>
</select>
</div>
</div>
<div class="control">
<button type="submit" class="button is-primary">
<span>Search</span>
<span class="icon icon-search" aria-hidden="true"></span>
</button>
</div>
</div>
</form>
{% if query %}
<nav class="tabs">
<ul>
<li{% if type == "book" %} class="is-active"{% endif %}>
<a href="{% url 'search' %}?q={{ query }}&type=book">{% trans "Books" %}</a>
</li>
{% if request.user.is_authenticated %}
<li{% if type == "user" %} class="is-active"{% endif %}>
<a href="{% url 'search' %}?q={{ query }}&type=user">{% trans "Users" %}</a>
</li>
{% endif %}
<li{% if type == "list" %} class="is-active"{% endif %}>
<a href="{% url 'search' %}?q={{ query }}&type=list">{% trans "Lists" %}</a>
</li>
</ul>
</nav>
<section class="block">
{% if not results %}
<p>
<em>{% blocktrans %}No results found for "{{ query }}"{% endblocktrans %}</em>
</p>
{% endif %}
{% block panel %}
{% endblock %}
<div>
{% include 'snippets/pagination.html' with page=results path=request.path %}
</div>
</section>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,7 @@
{% extends 'search/layout.html' %}
{% block panel %}
{% include 'lists/list_items.html' with lists=results %}
{% endblock %}

View file

@ -0,0 +1,13 @@
{% extends 'search/layout.html' %}
{% block panel %}
<div class="columns is-multiline">
{% for user in results %}
<div class="column is-one-third">
{% include 'directory/user_card.html' %}
</div>
{% endfor %}
</div>
{% endblock %}

View file

@ -1,108 +0,0 @@
{% extends 'layout.html' %}
{% load i18n %}
{% block title %}{% trans "Search Results" %}{% endblock %}
{% block content %}
{% with book_results|first as local_results %}
<div class="block">
<h1 class="title">{% blocktrans %}Search Results for "{{ query }}"{% endblocktrans %}</h1>
</div>
<div class="block columns">
<div class="column">
<h2 class="title">{% trans "Matching Books" %}</h2>
<section class="block">
{% if not local_results.results %}
<p>{% blocktrans %}No books found for "{{ query }}"{% endblocktrans %}</p>
{% else %}
<ul>
{% for result in local_results.results %}
<li class="pd-4">
{% include 'snippets/search_result_text.html' with result=result %}
</li>
{% endfor %}
</ul>
{% endif %}
</section>
{% if request.user.is_authenticated %}
{% if book_results|slice:":1" and local_results.results %}
<div class="block">
<p>
{% trans "Didn't find what you were looking for?" %}
</p>
{% trans "Show results from other catalogues" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text small=True controls_text="more-results" %}
</div>
{% endif %}
<div class="{% if local_results.results %}is-hidden{% endif %}" id="more-results">
{% for result_set in book_results|slice:"1:" %}
{% if result_set.results %}
<section class="block">
{% if not result_set.connector.local %}
<h3 class="title is-5">
Results from <a href="{{ result_set.connector.base_url }}" target="_blank">{% if result_set.connector.name %}{{ result_set.connector.name }}{% else %}{{ result_set.connector.identifier }}{% endif %}</a>
</h3>
{% endif %}
<ul>
{% for result in result_set.results %}
<li class="pb-4">
{% include 'snippets/search_result_text.html' with result=result remote_result=True %}
</li>
{% endfor %}
</ul>
</section>
{% endif %}
{% endfor %}
{% if local_results.results %}
{% trans "Hide results from other catalogues" as button_text %}
{% include 'snippets/toggle/close_button.html' with text=button_text small=True controls_text="more-results" %}
{% endif %}
</div>
<div class="block">
<a href="/create-book">Manually add book</a>
</div>
{% endif %}
</div>
<div class="column">
<section class="block">
<h2 class="title">{% trans "Matching Users" %}</h2>
{% if not user_results %}
<p>{% blocktrans %}No users found for "{{ query }}"{% endblocktrans %}</p>
{% endif %}
<ul>
{% for result in user_results %}
<li class="block">
<a href="{{ result.local_path }}">
{% include 'snippets/avatar.html' with user=result %}
{{ result.display_name }}
</a> ({{ result.username }})
{% include 'snippets/follow_button.html' with user=result %}
</li>
{% endfor %}
</ul>
</section>
<section class="block">
<h2 class="title">{% trans "Lists" %}</h2>
{% if not list_results %}
<p>{% blocktrans %}No lists found for "{{ query }}"{% endblocktrans %}</p>
{% endif %}
{% for result in list_results %}
<div class="block">
<ul>
<li>
<a href="{% url 'list' result.id %}">{{ result.name }}</a>
</li>
</ul>
</div>
{% endfor %}
</section>
</div>
</div>
{% endwith %}
{% endblock %}

View file

@ -43,6 +43,10 @@
{% if perms.bookwyrm.edit_instance_settings %}
<h2 class="menu-label">{% trans "Instance Settings" %}</h2>
<ul class="menu-list">
<li>
{% url 'settings-announcements' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Announcements" %}</a>
</li>
<li>
{% url 'settings-site' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Site Settings" %}</a>

View file

@ -0,0 +1,70 @@
{% extends 'settings/admin_layout.html' %}
{% load i18n %}{% load humanize %}
{% block title %}{% trans "Announcement" %} - {{ announcement.preview }}{% endblock %}
{% block header %}
{% trans "Announcement" %}
<a href="{% url 'settings-announcements' %}" class="has-text-weight-normal help">{% trans "Back to list" %}</a>
{% endblock %}
{% block edit-button %}
{% trans "Edit Announcement" as button_text %}
<div class="field has-addons">
<div class="control">
{% include 'snippets/toggle/open_button.html' with controls_text="edit-announcement" icon_with_text="pencil" text=button_text focus="edit-announcement-header" %}
</div>
<form class="control" action="{% url 'settings-announcements-delete' announcement.id %}" method="post">
{% csrf_token %}
<button type="submit" class="button is-danger">
<span class="icon icon-x" aria-hidden="true"></span>
<span>{% trans "Delete" %}</span>
</button>
</form>
</div>
{% endblock %}
{% block panel %}
<form name="edit-announcement" method="post" action="{% url 'settings-announcements' announcement.id %}" class="block">
{% include 'settings/announcement_form.html' with controls_text="edit-announcement" %}
</form>
<div class="block content">
<dl>
<div class="is-flex notification pt-1 pb-1 mb-0 {% if announcement in active_announcements %}is-success{% else %}is-danger{% endif %}">
<dt class="mr-1 has-text-weight-bold">{% trans "Visible:" %}</dt>
<dd>
{% if announcement in active_announcements %}
{% trans "True" %}
{% else %}
{% trans "False" %}
{% endif %}
</dd>
</div>
{% if announcement.start_date %}
<div class="is-flex notificationi pt-1 pb-1 mb-0 has-background-white">
<dt class="mr-1 has-text-weight-bold">{% trans "Start date:" %}</dt>
<dd>{{ announcement.start_date|naturalday }}</dd>
</div>
{% endif %}
{% if announcement.end_date %}
<div class="is-flex notification pt-1 pb-1 mb-0 has-background-white">
<dt class="mr-1 has-text-weight-bold">{% trans "End date:" %}</dt>
<dd>{{ announcement.end_date|naturalday }}</dd>
</div>
{% endif %}
<div class="is-flex notification pt-1 pb-1 has-background-white">
<dt class="mr-1 has-text-weight-bold">{% trans "Active:" %}</dt>
<dd>{{ announcement.active }}</dd>
</div>
</dl>
<hr aria-hidden="true">
{% include 'snippets/announcement.html' with announcement=announcement pressed=True admin_mode=True %}
</div>
{% endblock %}

View file

@ -0,0 +1,68 @@
{% extends 'components/inline_form.html' %}
{% load i18n %}
{% block header %}
{% trans "Create Announcement" %}
{% endblock %}
{% block form %}
{% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}">
<p>
<label class="label" for="id_preview">Preview:</label>
{{ form.preview }}
{% for error in form.preview.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</p>
<p>
<label class="label" for="id_content">Content:</label>
{{ form.content }}
{% for error in form.content.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</p>
<p>
<label class="label" for="id_event_date">Event date:</label>
<input type="date" name="event_date" value="{{ form.event_date.value }}" class="input" id="id_event_date">
{% for error in form.event_date.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</p>
<hr aria-hidden="true">
<div class="columns">
<div class="column">
<p>
<label class="label" for="id_start_date">Start date:</label>
<input type="date" name="start_date" class="input" id="id_start_date">
{% for error in form.start_date.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</p>
</div>
<div class="column">
<p>
<label class="label" for="id_end_date">End date:</label>
<input type="date" name="end_date" class="input" id="id_end_date">
{% for error in form.end_date.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</p>
</div>
<div class="column is-narrow">
<p>
<label class="label" for="id_active">Active:</label>
{{ form.active }}
{% for error in form.active.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</p>
</div>
</div>
<div class="field has-addons">
<div class="control">
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,54 @@
{% extends 'settings/admin_layout.html' %}
{% load i18n %}{% load humanize %}
{% block title %}{% trans "Announcements" %}{% endblock %}
{% block header %}{% trans "Announcements" %}{% endblock %}
{% block edit-button %}
{% trans "Create Announcement" as button_text %}
{% include 'snippets/toggle/open_button.html' with controls_text="create-announcement" icon_with_text="plus" text=button_text focus="create-announcement-header" %}
</a>
{% endblock %}
{% block panel %}
<form name="create-announcement" method="post" action="{% url 'settings-announcements' %}" class="block">
{% include 'settings/announcement_form.html' with controls_text="create-announcement" %}
</form>
<table class="table is-striped">
<tr>
<th>
{% url 'settings-announcements' as url %}
{% trans "Date added" as text %}
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
</th>
<th>
{% trans "Preview" as text %}
{% include 'snippets/table-sort-header.html' with field="preview" sort=sort text=text %}
</th>
<th>
{% trans "Start date" as text %}
{% include 'snippets/table-sort-header.html' with field="start_date" sort=sort text=text %}
</th>
<th>
{% trans "End date" as text %}
{% include 'snippets/table-sort-header.html' with field="end_date" sort=sort text=text %}
</th>
<th>
{% trans "Status" as text %}
{% include 'snippets/table-sort-header.html' with field="active" sort=sort text=text %}
</th>
</tr>
{% for announcement in announcements %}
<tr>
<td>{{ announcement.created_date|naturalday }}</td>
<td><a href="{% url 'settings-announcements' announcement.id %}">{{ announcement.preview }}</a></td>
<td>{{ announcement.start_date|naturaltime|default:'' }}</td>
<td>{{ announcement.end_date|naturaltime|default:'' }}</td>
<td>{% if announcement.active %}{% trans "active" %}{% else %}{% trans "inactive" %}{% endif %}</td>
</tr>
{% endfor %}
</table>
{% include 'snippets/pagination.html' with page=announcements path=request.path %}
{% endblock %}

View file

@ -1,7 +1,8 @@
{% extends 'settings/admin_layout.html' %}
{% block title %}{{ server.server_name }}{% endblock %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load markdown %}
{% block title %}{{ server.server_name }}{% endblock %}
{% block header %}
{{ server.server_name }}
@ -9,13 +10,14 @@
{% if server.status == "blocked" %}<span class="icon icon-x has-text-danger is-size-5" title="{% trans 'Blocked' %}"><span class="is-sr-only">{% trans "Blocked" %}</span></span>
{% endif %}
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to server list" %}</a>
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to list" %}</a>
{% endblock %}
{% block panel %}
<div class="columns">
<section class="column is-half content">
<section class="column is-half is-flex is-flex-direction-column">
<h2 class="title is-4">{% trans "Details" %}</h2>
<div class="box is-flex-grow-1 content">
<dl>
<div class="is-flex">
<dt>{% trans "Software:" %}</dt>
@ -30,10 +32,12 @@
<dd>{{ server.status }}</dd>
</div>
</dl>
</div>
</section>
<section class="column is-half content">
<section class="column is-half is-flex is-flex-direction-column">
<h2 class="title is-4">{% trans "Activity" %}</h2>
<div class="box is-flex-grow-1 content">
<dl>
<div class="is-flex">
<dt>{% trans "Users:" %}</dt>
@ -68,6 +72,7 @@
</dd>
</div>
</dl>
</div>
</section>
</div>
@ -78,11 +83,11 @@
</div>
<div class="column is-narrow">
{% trans "Edit" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="edit-notes" %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit-notes" %}
</div>
</header>
{% if server.notes %}
<p id="hide-edit-notes">{{ server.notes|to_markdown|safe }}</p>
<div class="box" id="hide-edit-notes">{{ server.notes|to_markdown|safe }}</div>
{% endif %}
<form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit-notes">
{% csrf_token %}

View file

@ -6,9 +6,8 @@
{% block edit-button %}
<a href="{% url 'settings-import-blocklist' %}">
<span class="icon icon-plus" title="{% trans 'Add server' %}">
<span class="is-sr-only">{% trans "Add server" %}</span>
</span>
<span class="icon icon-plus" title="{% trans 'Add server' %}" aria-hidden="True"></span>
<span>{% trans "Add server" %}</span>
</a>
{% endblock %}

View file

@ -37,16 +37,16 @@
<section class="block" id="images">
<h2 class="title is-4">{% trans "Images" %}</h2>
<div class="field is-grouped">
<div class="control">
<div class="columns">
<div class="column">
<label class="label" for="id_logo">{% trans "Logo:" %}</label>
{{ site_form.logo }}
</div>
<div class="control">
<div class="column">
<label class="label" for="id_logo_small">{% trans "Logo small:" %}</label>
{{ site_form.logo_small }}
</div>
<div class="control">
<div class="column">
<label class="label" for="id_favicon">{% trans "Favicon:" %}</label>
{{ site_form.favicon }}
</div>
@ -69,6 +69,10 @@
<label class="label" for="id_admin_email">{% trans "Admin email:" %}</label>
{{ site_form.admin_email }}
</div>
<div class="control">
<label class="label" for="id_footer_item">{% trans "Additional info:" %}</label>
{{ site_form.footer_item }}
</div>
</section>
<hr aria-hidden="true">

View file

@ -0,0 +1,38 @@
{% load humanize %}{% load i18n %}{% load utilities %}
{% with announcement.id|uuid as uuid %}
<aside
class="notification mb-1 p-3{% if not admin_mode %} is-hidden{% endif %} transition-y"
{% if not admin_mode %}data-hide="hide-announcement-{{ announcement.id }}"{% endif %}
>
<div class="columns mb-0">
<div class="column pb-0">
{% if announcement.event_date %}
<strong>{{ announcement.event_date|naturalday|title }}:</strong>
{% endif %}
{{ announcement.preview }}
</div>
{% if announcement.content %}
<div class="column is-narrow pb-0">
{% trans "Open" as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text controls_text="announcement" class="is-small" controls_uid=uuid icon_with_text="arrow-down" %}
{% trans "Close" as button_text %}
{% include 'snippets/toggle/close_button.html' with text=button_text controls_text="announcement" class="is-small" controls_uid=uuid icon_with_text="arrow-up" %}
</div>
{% endif %}
</div>
{% if announcement.content %}
<div class="mb-2 mt-2 {% if not pressed %}is-hidden{% endif %}" id="announcement-{{ uuid }}">
<div class="box is-shadowless mb-0">
{{ announcement.content|safe }}
</div>
</div>
{% endif %}
<div class="is-flex mt-0 help">
<p>{% blocktrans with user_path=announcement.user.local_path username=announcement.user.display_name %}Posted by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p>
{% if not admin_mode %}
<span class="mr-2 ml-2" aria-hidden="true">&middot;</span>
<a class="set-display" data-id="hide-announcement-{{ announcement.id }}" data-value="true">{% trans "Dismiss message" %}</a>
{% endif %}
</div>
</aside>
{% endwith %}

View file

@ -5,7 +5,7 @@
{% endcomment %}
{% for author in book.authors.all %}
<a
href="/author/{{ author.id }}"
href="{{ author.local_path }}"
class="author"
itemprop="author"
itemscope

View file

@ -1,3 +1,2 @@
{% load bookwyrm_tags %}
<img class="avatar image {% if large %}is-96x96{% elif medium %}is-48x48{% else %}is-32x32{% endif %}" src="{% if user.avatar %}/images/{{ user.avatar }}{% else %}/static/images/default_avi.jpg{% endif %}" {% if ariaHide %}aria-hidden="true"{% endif %} alt="{{ user.alt_text }}">

View file

@ -1,29 +1,41 @@
{% spaceless %}
{% load bookwyrm_tags %}
{% load i18n %}
<div class="cover-container is-{{ size }}">
{% if book.cover %}
<img
class="book-cover"
src="/images/{{ book.cover }}"
alt="{{ book.alt_text }}"
<figure
class="
cover-container
{{ cover_class }}
{% if not book.cover %}
no-cover
{% endif %}
"
{% if book.alt_text %}
title="{{ book.alt_text }}"
itemprop="thumbnailUrl"
{% endif %}
>
{% else %}
<div class="no-cover book-cover">
<img
class="book-cover"
{% if book.cover %}
src="{% if img_path is None %}/images/{% else %}{{ img_path }}{% endif %}{{ book.cover }}"
itemprop="thumbnailUrl"
{% if book.alt_text %}
alt="{{ book.alt_text }}"
{% endif %}
{% else %}
src="/static/images/no_cover.jpg"
alt="{% trans "No cover" %}"
{% endif %}
>
<div>
{% if not book.cover and book.alt_text %}
<figcaption class="cover_caption">
<p>{{ book.alt_text }}</p>
</div>
</div>
</figcaption>
{% endif %}
</div>
</figure>
{% endspaceless %}

View file

@ -1,13 +0,0 @@
<div class="columns is-multiline">
{% for book in books %}
<div class="column is-narrow">
<div class="box">
<a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %}
</a>
{% include 'snippets/shelve_button/shelve_button.html' with book=book switch_mode=True %}
</div>
</div>
{% endfor %}
</div>

View file

@ -1,8 +1,8 @@
{% load i18n %}
{% load bookwyrm_tags %}
{% load utilities %}
{% if book.authors %}
{% blocktrans with path=book.local_path title=book|title %}<a href="{{ path }}">{{ title }}</a> by {% endblocktrans %}{% include 'snippets/authors.html' with book=book %}
{% blocktrans with path=book.local_path title=book|book_title %}<a href="{{ path }}">{{ title }}</a> by {% endblocktrans %}{% include 'snippets/authors.html' with book=book %}
{% else %}
<a href="{{ book.local_path }}">{{ book|title }}</a>
<a href="{{ book.local_path }}">{{ book|book_title }}</a>
{% endif %}

View file

@ -1,4 +1,5 @@
{% load bookwyrm_tags %}
{% load interaction %}
{% load utilities %}
{% load i18n %}
{% with status.id|uuid as uuid %}

View file

@ -1,6 +1,6 @@
{% load humanize %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load utilities %}
{% with status_type=request.GET.status_type %}
<div class="tab-group">

View file

@ -1,4 +1,7 @@
{% load bookwyrm_tags %}
{% load utilities %}
{% load status_display %}
{% load i18n %}
<form class="is-flex-grow-1" name="{{ type }}" action="/post/{% if type == 'direct' %}status{% else %}{{ type }}{% endif %}" method="post" id="tab-{{ type }}-{{ book.id }}{{ reply_parent.id }}">
{% csrf_token %}
@ -36,13 +39,23 @@
<div class="control">
{% if type == 'quotation' %}
<textarea name="quote" class="textarea" id="id_quote_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required>{{ draft.quote|default:'' }}</textarea>
{% elif type == 'reply' %}
{% include 'snippets/content_warning_field.html' with parent_status=status %}
<textarea name="content" class="textarea" id="id_content_{{ type }}-{{ book.id }}{{reply_parent.id}}" placeholder="{{ placeholder }}" aria-label="{% trans 'Reply' %}" required>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mentions %}@{{ mentions|username }} {% endif %}{{ draft.content|default:'' }}</textarea>
<textarea
name="quote"
class="textarea"
id="id_quote_{{ book.id }}_{{ type }}"
placeholder="{{ placeholder }}"
required
>{{ draft.quote|default:'' }}</textarea>
{% else %}
{% include 'snippets/content_warning_field.html' with parent_status=status %}
<textarea name="content" class="textarea" id="id_content_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required>{{ draft.content|default:'' }}</textarea>
<textarea
name="content"
class="textarea"
id="id_content_{{ type }}-{{ book.id }}{{reply_parent.id}}"
placeholder="{{ placeholder }}"
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans "Content" %}{% endif %}"
required
>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mention %}@{{ mention|username }} {% endif %}{{ draft.content|default:'' }}</textarea>
{% endif %}
</div>
</div>

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