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

View file

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

View file

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

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. 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: 2. The User is one of the following:
a. An individual person, laboring for themselves
b. A non-profit organization 1. An individual person, laboring for themselves
c. An educational institution 2. A non-profit organization
d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor 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. 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) @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""" """serializes an edition or work, abstract"""
title: str title: str
lastEditedBy: str = None
sortTitle: str = "" sortTitle: str = ""
subtitle: str = "" subtitle: str = ""
description: str = "" description: str = ""
@ -25,10 +36,6 @@ class Book(ActivityObject):
firstPublishedDate: str = "" firstPublishedDate: str = ""
publishedDate: str = "" publishedDate: str = ""
openlibraryKey: str = ""
librarythingKey: str = ""
goodreadsKey: str = ""
cover: Document = None cover: Document = None
type: str = "Book" type: str = "Book"
@ -55,23 +62,21 @@ class Work(Book):
"""work instance of a book object""" """work instance of a book object"""
lccn: str = "" lccn: str = ""
defaultEdition: str = ""
editions: List[str] = field(default_factory=lambda: []) editions: List[str] = field(default_factory=lambda: [])
type: str = "Work" type: str = "Work"
@dataclass(init=False) @dataclass(init=False)
class Author(ActivityObject): class Author(BookData):
"""author of a book""" """author of a book"""
name: str name: str
lastEditedBy: str = None isni: str = None
viafId: str = None
gutenbergId: str = None
born: str = None born: str = None
died: str = None died: str = None
aliases: List[str] = field(default_factory=lambda: []) aliases: List[str] = field(default_factory=lambda: [])
bio: str = "" bio: str = ""
openlibraryKey: str = ""
librarythingKey: str = ""
goodreadsKey: str = ""
wikipediaLink: str = "" wikipediaLink: str = ""
type: str = "Author" type: str = "Author"

View file

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

View file

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

View file

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

View file

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

View file

@ -19,7 +19,7 @@ class ConnectorException(HTTPError):
"""when the connector can't do what was asked""" """when the connector can't do what was asked"""
def search(query, min_confidence=0.1): def search(query, min_confidence=0.1, return_first=False):
"""find books based on arbitary keywords""" """find books based on arbitary keywords"""
if not query: if not query:
return [] return []
@ -29,23 +29,18 @@ def search(query, min_confidence=0.1):
isbn = re.sub(r"[\W_]", "", query) isbn = re.sub(r"[\W_]", "", query)
maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13 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(): for connector in get_connectors():
result_set = None result_set = None
if maybe_isbn: if maybe_isbn and connector.isbn_search_url and connector.isbn_search_url == "":
# Search on ISBN # Search on ISBN
if not connector.isbn_search_url or connector.isbn_search_url == "": try:
result_set = [] result_set = connector.isbn_search(isbn)
else: except Exception as e: # pylint: disable=broad-except
try: logger.exception(e)
result_set = connector.isbn_search(isbn) # if this fails, we can still try regular search
except Exception as e: # pylint: disable=broad-except
logger.exception(e)
continue
# if no isbn search or results, we fallback to generic search # if no isbn search results, we fallback to generic search
if result_set in (None, []): if not result_set:
try: try:
result_set = connector.search(query, min_confidence=min_confidence) result_set = connector.search(query, min_confidence=min_confidence)
except Exception as e: # pylint: disable=broad-except except Exception as e: # pylint: disable=broad-except
@ -53,24 +48,30 @@ def search(query, min_confidence=0.1):
logger.exception(e) logger.exception(e)
continue continue
# if the search results look the same, ignore them if return_first and result_set:
result_set = [r for r in result_set if dedup_slug(r) not in result_index] # if we found anything, return it
# `|=` concats two sets. WE ARE GETTING FANCY HERE return result_set[0]
result_index |= set(dedup_slug(r) for r in result_set)
results.append( if result_set or connector.local:
{ results.append(
"connector": connector, {
"results": result_set, "connector": connector,
} "results": result_set,
) }
)
if return_first:
return None
return results 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""" """only look at local search results"""
connector = load_connector(models.Connector.objects.get(local=True)) 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): 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): def first_search_result(query, min_confidence=0.1):
"""search until you find a result that fits""" """search until you find a result that fits"""
for connector in get_connectors(): return search(query, min_confidence=min_confidence, return_first=True) or None
result = connector.search(query, min_confidence=min_confidence)
if result:
return result[0]
return None
def get_connectors(): def get_connectors():
"""load all 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) 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): def __init__(self, identifier):
super().__init__(identifier) super().__init__(identifier)
get_first = lambda a: a[0] get_first = lambda a, *args: a[0]
get_remote_id = lambda a: self.base_url + a get_remote_id = lambda a, *args: self.base_url + a
self.book_mappings = [ self.book_mappings = [
Mapping("title"), Mapping("title"),
Mapping("id", remote_field="key", formatter=get_remote_id), Mapping("id", remote_field="key", formatter=get_remote_id),
@ -58,6 +58,13 @@ class Connector(AbstractConnector):
Mapping("bio", formatter=get_description), 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): def get_remote_id_from_data(self, data):
"""format a url from an openlibrary id field""" """format a url from an openlibrary id field"""
try: try:
@ -75,8 +82,11 @@ class Connector(AbstractConnector):
except KeyError: except KeyError:
raise ConnectorException("Invalid book data") raise ConnectorException("Invalid book data")
url = "%s%s/editions" % (self.books_url, key) url = "%s%s/editions" % (self.books_url, key)
data = get_data(url) data = self.get_book_data(url)
return pick_default_edition(data["entries"]) 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): def get_work_from_edition_data(self, data):
try: try:
@ -84,7 +94,7 @@ class Connector(AbstractConnector):
except (IndexError, KeyError): except (IndexError, KeyError):
raise ConnectorException("No work found for edition") raise ConnectorException("No work found for edition")
url = "%s%s" % (self.books_url, key) url = "%s%s" % (self.books_url, key)
return get_data(url) return self.get_book_data(url)
def get_authors_from_data(self, data): def get_authors_from_data(self, data):
"""parse author json and load or create authors""" """parse author json and load or create authors"""
@ -143,7 +153,7 @@ class Connector(AbstractConnector):
def load_edition_data(self, olkey): def load_edition_data(self, olkey):
"""query openlibrary for editions of a work""" """query openlibrary for editions of a work"""
url = "%s/works/%s/editions" % (self.books_url, olkey) 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): def expand_book_data(self, book):
work = book work = book

View file

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

View file

@ -1,3 +1,3 @@
""" settings book data connectors """ """ 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 def site_settings(request): # pylint: disable=unused-argument
"""include the custom info about the site""" """include the custom info about the site"""
return {"site": models.SiteSettings.objects.get()} return {
"site": models.SiteSettings.objects.get(),
"active_announcements": models.Announcement.active_announcements(),
}

View file

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

View file

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

View file

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

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.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -7,12 +8,14 @@ from bookwyrm.settings import DOMAIN
def init_groups(): def init_groups():
"""permission levels"""
groups = ["admin", "moderator", "editor"] groups = ["admin", "moderator", "editor"]
for group in groups: for group in groups:
Group.objects.create(name=group) Group.objects.create(name=group)
def init_permissions(): def init_permissions():
"""permission types"""
permissions = [ permissions = [
{ {
"codename": "edit_instance_settings", "codename": "edit_instance_settings",
@ -69,6 +72,7 @@ def init_permissions():
def init_connectors(): def init_connectors():
"""access book data sources"""
Connector.objects.create( Connector.objects.create(
identifier=DOMAIN, identifier=DOMAIN,
name="Local", name="Local",
@ -94,6 +98,18 @@ def init_connectors():
priority=2, 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( Connector.objects.create(
identifier="openlibrary.org", identifier="openlibrary.org",
name="OpenLibrary", name="OpenLibrary",
@ -118,7 +134,11 @@ def init_federated_servers():
def init_settings(): 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): 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 .import_job import ImportJob, ImportItem
from .site import SiteSettings, SiteInvite, PasswordReset, InviteRequest from .site import SiteSettings, SiteInvite, PasswordReset, InviteRequest
from .announcement import Announcement
cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass) cls_members = inspect.getmembers(sys.modules[__name__], inspect.isclass)
activity_models = { 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( wikipedia_link = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True 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? # idk probably other keys would be useful here?
born = fields.DateTimeField(blank=True, null=True) born = fields.DateTimeField(blank=True, null=True)
died = 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 from .fields import RemoteIdField
DeactivationReason = models.TextChoices(
"DeactivationReason",
[
"self_deletion",
"moderator_deletion",
"domain_block",
],
)
class BookWyrmModel(models.Model): class BookWyrmModel(models.Model):
"""shared fields""" """shared fields"""

View file

@ -1,11 +1,11 @@
""" database schema for books and shelves """ """ database schema for books and shelves """
import re import re
from django.db import models, transaction from django.db import models
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN, DEFAULT_LANGUAGE
from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
@ -19,12 +19,18 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
openlibrary_key = fields.CharField( openlibrary_key = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True 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( librarything_key = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True max_length=255, blank=True, null=True, deduplication_field=True
) )
goodreads_key = fields.CharField( goodreads_key = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True 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( last_edited_by = fields.ForeignKey(
"User", "User",
@ -137,10 +143,6 @@ class Work(OrderedCollectionPageMixin, Book):
lccn = fields.CharField( lccn = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True 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): def save(self, *args, **kwargs):
"""set some fields on the edition object""" """set some fields on the edition object"""
@ -149,18 +151,10 @@ class Work(OrderedCollectionPageMixin, Book):
edition.save() edition.save()
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
def get_default_edition(self): @property
def default_edition(self):
"""in case the default edition is not set""" """in case the default edition is not set"""
return self.default_edition or self.editions.order_by("-edition_rank").first() return 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()
def to_edition_list(self, **kwargs): def to_edition_list(self, **kwargs):
"""an ordered collection of editions""" """an ordered collection of editions"""
@ -214,17 +208,20 @@ class Edition(Book):
activity_serializer = activitypub.Edition activity_serializer = activitypub.Edition
name_field = "title" name_field = "title"
def get_rank(self, ignore_default=False): def get_rank(self):
"""calculate how complete the data is on this edition""" """calculate how complete the data is on this edition"""
if (
not ignore_default
and self.parent_work
and self.parent_work.default_edition == self
):
# default edition has the highest rank
return 20
rank = 0 rank = 0
# big ups for havinga cover
rank += int(bool(self.cover)) * 3 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_13))
rank += int(bool(self.isbn_10)) rank += int(bool(self.isbn_10))
rank += int(bool(self.oclc_number)) rank += int(bool(self.oclc_number))
@ -242,6 +239,12 @@ class Edition(Book):
if self.isbn_10 and not self.isbn_13: if self.isbn_10 and not self.isbn_13:
self.isbn_13 = isbn_10_to_13(self.isbn_10) 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 # set rank
self.edition_rank = self.get_rank() self.edition_rank = self.get_rank()

View file

@ -2,7 +2,7 @@
from django.db import models from django.db import models
from bookwyrm.connectors.settings import CONNECTORS from bookwyrm.connectors.settings import CONNECTORS
from .base_model import BookWyrmModel from .base_model import BookWyrmModel, DeactivationReason
ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS) ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS)
@ -17,6 +17,10 @@ class Connector(BookWyrmModel):
local = models.BooleanField(default=False) local = models.BooleanField(default=False)
connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices) connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)
api_key = models.CharField(max_length=255, null=True, blank=True) 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) base_url = models.CharField(max_length=255)
books_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) search_url = models.CharField(max_length=255, null=True, blank=True)
isbn_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): def __str__(self):
return "{} ({})".format( return "{} ({})".format(
self.identifier, self.identifier,

View file

@ -1,5 +1,6 @@
""" connections to external ActivityPub servers """ """ connections to external ActivityPub servers """
from urllib.parse import urlparse from urllib.parse import urlparse
from django.apps import apps
from django.db import models from django.db import models
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
@ -34,6 +35,13 @@ class FederatedServer(BookWyrmModel):
is_active=False, deactivation_reason="domain_block" 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): def unblock(self):
"""unblock a server""" """unblock a server"""
self.status = "federated" self.status = "federated"
@ -43,6 +51,15 @@ class FederatedServer(BookWyrmModel):
is_active=True, deactivation_reason=None 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 @classmethod
def is_blocked(cls, url): def is_blocked(cls, url):
"""look up if a domain is blocked""" """look up if a domain is blocked"""

View file

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

View file

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

View file

@ -19,21 +19,11 @@ from bookwyrm.signatures import create_key_pair
from bookwyrm.tasks import app from bookwyrm.tasks import app
from bookwyrm.utils import regex from bookwyrm.utils import regex
from .activitypub_mixin import OrderedCollectionPageMixin, ActivitypubMixin from .activitypub_mixin import OrderedCollectionPageMixin, ActivitypubMixin
from .base_model import BookWyrmModel from .base_model import BookWyrmModel, DeactivationReason
from .federated_server import FederatedServer from .federated_server import FederatedServer
from . import fields, Review from . import fields, Review
DeactivationReason = models.TextChoices(
"DeactivationReason",
[
"self_deletion",
"moderator_deletion",
"domain_block",
],
)
class User(OrderedCollectionPageMixin, AbstractUser): class User(OrderedCollectionPageMixin, AbstractUser):
"""a user who wants to read books""" """a user who wants to read books"""
@ -150,6 +140,19 @@ class User(OrderedCollectionPageMixin, AbstractUser):
"""for consistent naming""" """for consistent naming"""
return not self.is_active 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 activity_serializer = activitypub.Person
@classmethod @classmethod
@ -359,7 +362,10 @@ class AnnualGoal(BookWyrmModel):
def books(self): def books(self):
"""the books you've read this year""" """the books you've read this year"""
return ( 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") .order_by("-finish_date")
.all() .all()
) )
@ -383,7 +389,8 @@ class AnnualGoal(BookWyrmModel):
def book_count(self): def book_count(self):
"""how many books you've read this year""" """how many books you've read this year"""
return self.user.readthrough_set.filter( 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() ).count()

View file

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

View file

@ -43,10 +43,6 @@ body {
white-space: nowrap !important; white-space: nowrap !important;
width: 0.01em !important; width: 0.01em !important;
} }
.m-0-mobile {
margin: 0 !important;
}
} }
.button.is-transparent { .button.is-transparent {
@ -92,10 +88,11 @@ body {
.transition-y.is-hidden { .transition-y.is-hidden {
display: block !important; display: block !important;
visibility: hidden !important; visibility: hidden !important;
height: 0; height: 0 !important;
width: 0; width: 0 !important;
margin: 0; margin: 0 !important;
padding: 0; padding: 0 !important;
overflow: auto;
} }
.transition-x, .transition-x,
@ -132,7 +129,7 @@ body {
* *
* \e9d9: filled star * \e9d9: filled star
* \e9d7: empty star; * \e9d7: empty star;
******************************************************************************/ * -------------------------------------------------------------------------- */
.form-rate-stars { .form-rate-stars {
width: max-content; width: max-content;
@ -158,70 +155,67 @@ body {
} }
/** Book covers /** 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 { .cover-container {
height: 250px; display: flex;
justify-content: center;
align-items: center;
position: relative;
width: max-content; width: max-content;
max-width: 250px; max-width: 100%;
overflow: hidden;
} }
.cover-container.is-large { /* Book cover
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%; display: block;
object-fit: scale-down; max-width: 100%;
max-height: 100%;
/* Useful when stretching under-sized images. */
image-rendering: optimizeQuality;
image-rendering: smooth;
} }
.no-cover { /* Cover caption
position: relative; * -------------------------------------------------------------------------- */
white-space: normal;
}
.no-cover div { .no-cover .cover_caption {
position: absolute; position: absolute;
padding: 1em;
color: white;
top: 0; top: 0;
right: 0;
bottom: 0;
left: 0; left: 0;
text-align: center; padding: 0.25em;
} font-size: 0.75em;
color: white;
.cover-container.is-medium .no-cover div { background-color: #002549;
font-size: 0.9em;
padding: 0.3em;
}
.cover-container.is-small .no-cover div {
font-size: 0.7em;
padding: 0.1em;
} }
/** Avatars /** Avatars
@ -232,16 +226,6 @@ body {
display: inline; display: inline;
} }
.is-32x32 {
min-width: 32px;
min-height: 32px;
}
.is-96x96 {
min-width: 96px;
min-height: 96px;
}
/** Statuses: Quotes /** Statuses: Quotes
* *
* \e906: icon-quote-open * \e906: icon-quote-open
@ -346,3 +330,386 @@ body {
display: none; 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) { updateCountElement(counter, data) {
const currentCount = counter.innerText; const currentCount = counter.innerText;
const count = data.count; const count = data.count;
const hasMentions = data.has_mentions;
if (count != currentCount) { if (count != currentCount) {
this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-hidden', count < 1); this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-hidden', count < 1);
counter.innerText = count; 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="columns">
<div class="column"> <div class="column">
<h2 class="title is-4">{% trans "Metadata" %}</h2> <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 %} {% for error in form.name.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.bio.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.wikipedia_link.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.born.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.died.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</div> </div>
<div class="column"> <div class="column">
<h2 class="title is-4">{% trans "Author Identifiers" %}</h2> <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 %} {% for error in form.openlibrary_key.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.librarything_key.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.goodreads_key.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
@ -76,7 +96,7 @@
<div class="block"> <div class="block">
<button class="button is-primary" type="submit">{% trans "Save" %}</button> <button class="button is-primary" type="submit">{% trans "Save" %}</button>
<a class="button" href="/author/{{ author.id }}">{% trans "Cancel" %}</a> <a class="button" href="{{ author.local_path }}">{% trans "Cancel" %}</a>
</div> </div>
</form> </form>

View file

@ -1,7 +1,5 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}{% load bookwyrm_tags %}{% load humanize %}{% load utilities %}
{% load bookwyrm_tags %}
{% load humanize %}
{% block title %}{{ book.title }}{% endblock %} {% block title %}{{ book.title }}{% endblock %}
@ -38,9 +36,8 @@
{% if user_authenticated and can_edit_book %} {% if user_authenticated and can_edit_book %}
<div class="column is-narrow"> <div class="column is-narrow">
<a href="{{ book.id }}/edit"> <a href="{{ book.id }}/edit">
<span class="icon icon-pencil" title="{% trans "Edit Book" %}"> <span class="icon icon-pencil" title="{% trans "Edit Book" %}" aria-hidden=True></span>
<span class="is-sr-only">{% trans "Edit Book" %}</span> <span>{% trans "Edit Book" %}</span>
</span>
</a> </a>
</div> </div>
{% endif %} {% endif %}
@ -48,10 +45,9 @@
<div class="columns"> <div class="columns">
<div class="column is-one-fifth"> <div class="column is-one-fifth">
<div class="is-clipped"> {% include 'snippets/book_cover.html' with book=book cover_class='is-h-m-mobile' %}
{% include 'snippets/book_cover.html' with book=book size=large %} {% include 'snippets/rate_action.html' with user=request.user book=book %}
{% include 'snippets/rate_action.html' with user=request.user book=book %}
</div>
<div class="mb-3"> <div class="mb-3">
{% include 'snippets/shelve_button/shelve_button.html' %} {% include 'snippets/shelve_button/shelve_button.html' %}
</div> </div>
@ -81,6 +77,9 @@
{% if book.openlibrary_key %} {% if book.openlibrary_key %}
<p><a href="https://openlibrary.org/books/{{ book.openlibrary_key }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a></p> <p><a href="https://openlibrary.org/books/{{ book.openlibrary_key }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a></p>
{% endif %} {% 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> </section>
</div> </div>
@ -153,17 +152,17 @@
</div> </div>
{% if user_authenticated %} {% if user_authenticated %}
<hr aria-hidden="true">
<section class="block"> <section class="block">
<header class="columns"> <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"> <div class="column is-narrow">
{% trans "Add read dates" as button_text %} {% 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> </div>
</header> </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"> <section class="is-hidden box" id="add-readthrough">
<form name="add-readthrough" action="/create-readthrough" method="post"> <form name="add-readthrough" action="/create-readthrough" method="post">
{% include 'snippets/readthrough_form.html' with readthrough=None %} {% include 'snippets/readthrough_form.html' with readthrough=None %}
@ -178,15 +177,75 @@
</div> </div>
</form> </form>
</section> </section>
{% if not readthroughs.exists %}
<p>{% trans "You don't have any reading activity for this book." %}</p>
{% endif %}
{% for readthrough in readthroughs %} {% for readthrough in readthroughs %}
{% include 'snippets/readthrough.html' with readthrough=readthrough %} {% include 'snippets/readthrough.html' with readthrough=readthrough %}
{% endfor %} {% endfor %}
</section> </section>
<hr aria-hidden="true">
<section class="box"> <section class="box">
{% include 'snippets/create_status.html' with book=book hide_cover=True %} {% include 'snippets/create_status.html' with book=book hide_cover=True %}
</section> </section>
{% endif %} {% 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>
<div class="column is-one-fifth"> <div class="column is-one-fifth">
{% if book.subjects %} {% if book.subjects %}
@ -245,80 +304,6 @@
</div> </div>
</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> </div>
{% endwith %} {% endwith %}
{% endblock %} {% endblock %}

View file

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

View file

@ -88,6 +88,7 @@
<div class="column is-half"> <div class="column is-half">
<section class="block"> <section class="block">
<h2 class="title is-4">{% trans "Metadata" %}</h2> <h2 class="title is-4">{% trans "Metadata" %}</h2>
<p class="mb-2"> <p class="mb-2">
<label class="label" for="id_title">{% trans "Title:" %}</label> <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"> <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> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.series_number.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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"> <p class="mb-2">
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label> <label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
{{ form.publishers }} {{ form.publishers }}
<span class="help">{% trans "Separate multiple publishers with commas." %}</span> <span class="help">{% trans "Separate multiple values with commas." %}</span>
</p> </p>
{% for error in form.publishers.errors %} {% for error in form.publishers.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
@ -162,17 +175,18 @@
{% endif %} {% endif %}
<label class="label" for="id_add_author">{% trans "Add Authors:" %}</label> <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 %}> <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> </section>
</div> </div>
<div class="column is-half"> <div class="column is-half">
<h2 class="title is-4">{% trans "Cover" %}</h2> <h2 class="title is-4">{% trans "Cover" %}</h2>
<div class="columns"> <div class="columns">
<div class="column is-narrow"> <div class="column is-3 is-cover">
{% include 'snippets/book_cover.html' with book=book size="small" %} {% include 'snippets/book_cover.html' with book=book cover_class='is-h-xl-mobile is-w-auto-tablet' %}
</div> </div>
<div class="column is-narrow">
<div class="column">
<div class="block"> <div class="block">
<p> <p>
<label class="label" for="id_cover">{% trans "Upload cover:" %}</label> <label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
@ -215,18 +229,27 @@
{% for error in form.isbn_13.errors %} {% for error in form.isbn_13.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
<p class="mb-2"><label class="label" for="id_isbn_10">{% trans "ISBN 10:" %}</label> {{ form.isbn_10 }} </p> <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 %} {% for error in form.isbn_10.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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 %} {% for error in form.openlibrary_key.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% 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> <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 %} {% for error in form.oclc_number.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
<p class="mb-2"><label class="label" for="id_asin">{% trans "ASIN:" %}</label> {{ form.asin }} </p> <p class="mb-2"><label class="label" for="id_asin">{% trans "ASIN:" %}</label> {{ form.asin }} </p>
{% for error in form.ASIN.errors %} {% for error in form.ASIN.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
@ -238,7 +261,7 @@
{% if not confirm_mode %} {% if not confirm_mode %}
<div class="block"> <div class="block">
<button class="button is-primary" type="submit">{% trans "Save" %}</button> <button class="button is-primary" type="submit">{% trans "Save" %}</button>
<a class="button" href="/book/{{ book.id }}">{% trans "Cancel" %}</a> <a class="button" href="{{ book.local_path}}">{% trans "Cancel" %}</a>
</div> </div>
{% endif %} {% endif %}
</form> </form>

View file

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

View file

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

View file

@ -1,7 +1,5 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
{% block title %}{% trans "Directory" %}{% endblock %} {% block title %}{% trans "Directory" %}{% endblock %}
@ -41,59 +39,7 @@
<div class="columns is-multiline"> <div class="columns is-multiline">
{% for user in users %} {% for user in users %}
<div class="column is-one-third"> <div class="column is-one-third">
<div class="card is-stretchable"> {% include 'directory/user_card.html' %}
<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>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@ -103,7 +49,3 @@
</div> </div>
{% endblock %} {% 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' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load markdown %}
{% block title %}{% trans "Welcome" %}{% endblock %} {% block title %}{% trans "Welcome" %}{% endblock %}
@ -49,7 +49,7 @@
{% else %} {% else %}
<h2 class="title">{% trans "This instance is closed" %}</h2> <h2 class="title">{% trans "This instance is closed" %}</h2>
<p>{{ site.registration_closed_text | safe}}</p> <p>{{ site.registration_closed_text|safe}}</p>
{% if site.allow_invite_requests %} {% if site.allow_invite_requests %}
{% if request_received %} {% if request_received %}
@ -64,7 +64,7 @@
<label for="id_request_email" class="label">{% trans "Email address:" %}</label> <label for="id_request_email" class="label">{% trans "Email address:" %}</label>
<input type="email" name="email" maxlength="255" class="input" required="" id="id_request_email"> <input type="email" name="email" maxlength="255" class="input" required="" id="id_request_email">
{% for error in request_form.email.errors %} {% for error in request_form.email.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error|escape }}</p>
{% endfor %} {% endfor %}
</div> </div>
<button type="submit" class="button is-link">{% trans "Submit" %}</button> <button type="submit" class="button is-link">{% trans "Submit" %}</button>
@ -80,7 +80,7 @@
{% include 'user/user_preview.html' with user=request.user %} {% include 'user/user_preview.html' with user=request.user %}
{% if request.user.summary %} {% if request.user.summary %}
<div class="box content"> <div class="box content">
{{ request.user.summary | to_markdown | safe }} {{ request.user.summary|to_markdown|safe }}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View file

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

View file

@ -1,12 +1,24 @@
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load i18n %} {% load i18n %}
{% if book %} {% if book %}
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a> {% with book=book %}
{% include 'snippets/stars.html' with rating=book|rating:request.user %} <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>
<h3 class="title is-6"><a href="/book/{{ book.id }}">{{ book.title }}</a></h3> {% include 'snippets/stars.html' with rating=book|rating:request.user %}
{% 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 %} {% endif %}

View file

@ -10,11 +10,11 @@
{% trans "Direct Messages" %} {% trans "Direct Messages" %}
{% endif %} {% endif %}
</h1> </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> </header>
<div class="box"> <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> </div>
<section class="block"> <section class="block">

View file

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

View file

@ -1,6 +1,5 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
{% block title %}{% trans "Updates" %}{% endblock %} {% block title %}{% trans "Updates" %}{% endblock %}
@ -37,7 +36,7 @@
aria-label="{{ book.title }}" 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-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 }}"> 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> </a>
</li> </li>
{% endfor %} {% endfor %}

View file

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

View file

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

View file

@ -1,8 +1,8 @@
{% load i18n %} {% load i18n %}
<div class="column is-narrow is-clipped has-text-centered"> <div class="column is-cover">
{% include 'snippets/book_cover.html' with book=book %} {% include 'snippets/book_cover.html' with book=book cover_class='is-h-l is-h-m-mobile' %}
<label class="label" for="id_shelve_{{ book.id }}">
<div class="select is-small"> <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 %}"> <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> <option disabled selected value>Add to your books</option>
{% for shelf in request.user.shelf_set.all %} {% for shelf in request.user.shelf_set.all %}

View file

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

View file

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

View file

@ -20,6 +20,9 @@
<option value="GoodReads" {% if current == 'GoodReads' %}selected{% endif %}> <option value="GoodReads" {% if current == 'GoodReads' %}selected{% endif %}>
GoodReads (CSV) GoodReads (CSV)
</option> </option>
<option value="Storygraph" {% if current == 'Storygraph' %}selected{% endif %}>
Storygraph (CSV)
</option>
<option value="LibraryThing" {% if current == 'LibraryThing' %}selected{% endif %}> <option value="LibraryThing" {% if current == 'LibraryThing' %}selected{% endif %}>
LibraryThing (TSV) LibraryThing (TSV)
</option> </option>
@ -56,7 +59,7 @@
{% endif %} {% endif %}
<ul> <ul>
{% for job in jobs %} {% 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 %} {% endfor %}
</ul> </ul>
</div> </div>

View file

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

View file

@ -1,5 +1,4 @@
{% load bookwyrm_tags %} {% load layout %}{% load i18n %}
{% load i18n %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{% get_lang %}"> <html lang="{% get_lang %}">
<head> <head>
@ -94,12 +93,12 @@
</a> </a>
</li> </li>
<li> <li>
<a href="/import" class="navbar-item"> <a href="{% url 'import' %}" class="navbar-item">
{% trans 'Import Books' %} {% trans 'Import Books' %}
</a> </a>
</li> </li>
<li> <li>
<a href="/preferences/profile" class="navbar-item"> <a href="{% url 'prefs-profile' %}" class="navbar-item">
{% trans 'Settings' %} {% trans 'Settings' %}
</a> </a>
</li> </li>
@ -122,21 +121,24 @@
{% endif %} {% endif %}
<li class="navbar-divider" role="presentation"></li> <li class="navbar-divider" role="presentation"></li>
<li> <li>
<a href="/logout" class="navbar-item"> <a href="{% url 'logout' %}" class="navbar-item">
{% trans 'Log out' %} {% trans 'Log out' %}
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
<div class="navbar-item"> <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="tag is-medium">
<span class="icon icon-bell" title="{% trans 'Notifications' %}"> <span class="icon icon-bell" title="{% trans 'Notifications' %}">
<span class="is-sr-only">{% trans "Notifications" %}</span> <span class="is-sr-only">{% trans "Notifications" %}</span>
</span> </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
<span data-poll="notifications">{{ request.user | notification_count }}</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> </span>
</a> </a>
</div> </div>
@ -155,7 +157,7 @@
<div class="column"> <div class="column">
<label class="is-sr-only" for="id_password">{% trans "Username:" %}</label> <label class="is-sr-only" for="id_password">{% trans "Username:" %}</label>
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password" placeholder="{% trans 'password' %}"> <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>
<div class="column is-narrow"> <div class="column is-narrow">
<button class="button is-primary" type="submit">{% trans "Log in" %}</button> <button class="button is-primary" type="submit">{% trans "Log in" %}</button>
@ -179,6 +181,15 @@
</div> </div>
</nav> </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="section is-flex-grow-1">
<div class="container"> <div class="container">
@ -190,25 +201,35 @@
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="container">
<div class="columns"> <div class="columns">
<div class="column"> <div class="column is-one-fifth">
<p> <p>
<a href="/about">{% trans "About this server" %}</a> <a href="{% url 'about' %}">{% trans "About this server" %}</a>
</p> </p>
{% if site.admin_email %} {% if site.admin_email %}
<p> <p>
<a href="mailto:{{ site.admin_email }}">{% trans "Contact site admin" %}</a> <a href="mailto:{{ site.admin_email }}">{% trans "Contact site admin" %}</a>
</p> </p>
{% endif %} {% endif %}
<p>
<a href="https://docs.joinbookwyrm.com/">{% trans "Documentation" %}</a>
</p>
</div> </div>
{% if site.support_link %} <div class="column content is-two-fifth">
{% if site.support_link %}
<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"> <div class="column">
<span class="icon icon-heart"></span> <p>{{ site.footer_item|safe }}</p>
{% 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 %}
</div> </div>
{% endif %} {% 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>
</div> </div>
</footer> </footer>
@ -217,6 +238,7 @@
var csrf_token = '{{ csrf_token }}'; var csrf_token = '{{ csrf_token }}';
</script> </script>
<script src="/static/js/bookwyrm.js"></script> <script src="/static/js/bookwyrm.js"></script>
<script src="/static/js/localstorage.js"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>
</html> </html>

View file

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

View file

@ -1,35 +1,64 @@
{% extends 'lists/list_layout.html' %} {% extends 'lists/list_layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load markdown %}
{% block panel %} {% block panel %}
{% if request.user == list.user and pending_count %} {% if request.user == list.user and pending_count %}
<div class="block content"> <div class="block content">
<p> <p>
<a href="{% url 'list-curate' list.id %}">{{ pending_count }} book{{ pending_count | pluralize }} awaiting your approval</a> <a href="{% url 'list-curate' list.id %}">{{ pending_count }} book{{ pending_count|pluralize }} awaiting your approval</a>
</p> </p>
</div> </div>
{% endif %} {% endif %}
<div class="columns mt-3"> <div class="columns mt-3">
<section class="column is-three-quarters"> <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 %} {% if not items.object_list.exists %}
<p>{% trans "This list is currently empty" %}</p> <p>{% trans "This list is currently empty" %}</p>
{% else %} {% else %}
<ol start="{{ items.start_index }}"> <ol start="{{ items.start_index }}">
{% for item in items %} {% for item in items %}
<li class="block pb-3"> <li class="block mb-5">
<div class="card"> <div class="card">
<div class="card-content columns p-0 pr-2 mb-0 is-mobile"> {% with book=item.book %}
<div class="column is-narrow pt-0 pb-0"> <div
<a href="{{ item.book.local_path }}">{% include 'snippets/book_cover.html' with book=item.book size="medium" %}</a> 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 ml-3">
<p>
{% include 'snippets/book_titleby.html' %}
</p>
<p>
{% include 'snippets/stars.html' with rating=item.book|rating:request.user %}
</p>
<div>
{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}
</div>
{% include 'snippets/shelve_button/shelve_button.html' %}
</div>
</div> </div>
<div class="column is-flex-direction-column is-align-items-self-start"> {% endwith %}
<span>{% include 'snippets/book_titleby.html' with book=item.book %}</span>
{% include 'snippets/stars.html' with rating=item.book|rating:request.user %}
{% include 'snippets/shelve_button/shelve_button.html' with book=item.book %}
</div>
</div>
<div class="card-footer has-background-white-bis is-align-items-baseline"> <div class="card-footer has-background-white-bis is-align-items-baseline">
<div class="card-footer-item"> <div class="card-footer-item">
<div> <div>
@ -66,7 +95,7 @@
{% include "snippets/pagination.html" with page=items %} {% include "snippets/pagination.html" with page=items %}
</section> </section>
<section class="column is-one-quarter content"> <section class="column is-one-quarter">
<h2>{% trans "Sort List" %}</h2> <h2>{% trans "Sort List" %}</h2>
<form name="sort" action="{% url 'list' list.id %}" method="GET" class="block"> <form name="sort" action="{% url 'list' list.id %}" method="GET" class="block">
<label class="label" for="id_sort_by">{% trans "Sort By" %}</label> <label class="label" for="id_sort_by">{% trans "Sort By" %}</label>
@ -108,24 +137,36 @@
<p>{% trans "No books found" %}</p> <p>{% trans "No books found" %}</p>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% for book in suggested_books %}
{% if book %} {% if suggested_books|length > 0 %}
<div class="block columns is-mobile"> {% for book in suggested_books %}
<div class="column is-narrow"> <div class="columns is-mobile is-gapless">
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a> <a
</div> class="column is-2-mobile is-3-tablet is-cover align to-c"
<div class="column"> href="{{ book.local_path }}"
<p>{% include 'snippets/book_titleby.html' with book=book %}</p> aria-hidden="true"
<form name="add-book" method="post" action="{% url 'list-add-book' %}"> >
{% csrf_token %} {% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto is-h-s-mobile align to-t' %}
<input type="hidden" name="book" value="{{ book.id }}"> </a>
<input type="hidden" name="list" value="{{ list.id }}">
<button type="submit" class="button is-small is-link">{% if list.curation == 'open' or request.user == list.user %}{% trans "Add" %}{% else %}{% trans "Suggest" %}{% endif %}</button> <div class="column ml-3">
</form> <p>{% include 'snippets/book_titleby.html' with book=book %}</p>
</div>
</div> <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 }}">
<button type="submit" class="button is-small is-link">{% if list.curation == 'open' or request.user == list.user %}{% trans "Add" %}{% else %}{% trans "Suggest" %}{% endif %}</button>
</form>
</div>
</div>
{% endfor %}
{% endif %} {% endif %}
{% endfor %}
{% endif %} {% endif %}
</section> </section>
</div> </div>

View file

@ -1,4 +1,4 @@
{% load bookwyrm_tags %} {% load markdown %}
<div class="columns is-multiline"> <div class="columns is-multiline">
{% for list in lists %} {% for list in lists %}
<div class="column is-one-quarter"> <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> <a href="{{ list.local_path }}">{{ list.name }}</a> <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>
</h4> </h4>
</header> </header>
<div class="card-image is-flex is-clipped">
{% for book in list.listitem_set.all|slice:5 %} {% with list_books=list.listitem_set.all|slice:5 %}
<a href="{{ book.book.local_path }}">{% include 'snippets/book_cover.html' with book=book.book size="small" %}</a> {% if list_books %}
{% endfor %} <div class="card-image columns is-mobile is-gapless is-clipped">
</div> {% 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 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 %} {% if list.description %}
{{ list.description|to_markdown|safe|truncatechars_html:30 }} {{ list.description|to_markdown|safe|truncatechars_html:30 }}
{% else %} {% else %}

View file

@ -1,6 +1,5 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
{% block title %}{{ list.name }}{% endblock %} {% block title %}{{ list.name }}{% endblock %}
@ -16,7 +15,7 @@
{% if request.user == list.user %} {% if request.user == list.user %}
<div class="column is-narrow"> <div class="column is-narrow">
{% trans "Edit List" as button_text %} {% 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> </div>
{% endif %} {% endif %}
</header> </header>

View file

@ -1,5 +1,5 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load bookwyrm_tags %} {% load utilities %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Lists" %}{% endblock %} {% block title %}{% trans "Lists" %}{% endblock %}
@ -18,7 +18,7 @@
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="column is-narrow"> <div class="column is-narrow">
{% trans "Create List" as button_text %} {% 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> </div>
{% endif %} {% endif %}
</header> </header>
@ -28,7 +28,7 @@
</div> </div>
{% if lists %} {% if lists %}
<section class="block content"> <section class="block">
{% include 'lists/list_items.html' with lists=lists %} {% include 'lists/list_items.html' with lists=lists %}
</section> </section>

View file

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

View file

@ -1,6 +1,5 @@
{% extends 'settings/admin_layout.html' %} {% extends 'settings/admin_layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %} {% load humanize %}
{% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %} {% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}
@ -29,7 +28,7 @@
<a href="{{ comment.user.local_path }}">{{ comment.user.display_name }}</a> <a href="{{ comment.user.local_path }}">{{ comment.user.display_name }}</a>
</div> </div>
<div class="card-footer-item"> <div class="card-footer-item">
{{ comment.created_date | naturaltime }} {{ comment.created_date|naturaltime }}
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,18 +1,35 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load humanize %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load humanize %}
{% block title %}{% trans "Notifications" %}{% endblock %} {% block title %}{% trans "Notifications" %}{% endblock %}
{% block content %} {% block content %}
<div class="block"> <header class="columns">
<h1 class="title">{% trans "Notifications" %}</h1> <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 %} {% csrf_token %}
<button class="button is-danger is-light" type="submit" class="secondary">{% trans "Delete notifications" %}</button> <button class="button is-danger is-light" type="submit" class="secondary">{% trans "Delete notifications" %}</button>
</form> </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>
<div class="block"> <div class="block">
@ -107,7 +124,8 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% elif notification.related_import %} {% 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 %} {% elif notification.related_report %}
{% url 'settings-report' notification.related_report.id as path %} {% url 'settings-report' notification.related_report.id as path %}
{% blocktrans with related_id=path %}A new <a href="{{ path }}">report</a> needs moderation.{% endblocktrans %} {% blocktrans with related_id=path %}A new <a href="{{ path }}">report</a> needs moderation.{% endblocktrans %}

View file

@ -8,7 +8,7 @@
{% endblock %} {% endblock %}
{% block panel %} {% 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 %} {% csrf_token %}
<div class="block"> <div class="block">
<label class="label" for="id_password">{% trans "New password:" %}</label> <label class="label" for="id_password">{% trans "New password:" %}</label>

View file

@ -11,16 +11,19 @@
<h2 class="menu-label">{% trans "Account" %}</h2> <h2 class="menu-label">{% trans "Account" %}</h2>
<ul class="menu-list"> <ul class="menu-list">
<li> <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>
<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> </li>
</ul> </ul>
<h2 class="menu-label">{% trans "Relationships" %}</h2> <h2 class="menu-label">{% trans "Relationships" %}</h2>
<ul class="menu-list"> <ul class="menu-list">
<li> <li>
<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> </li>
</ul> </ul>
</nav> </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 %} {% if perms.bookwyrm.edit_instance_settings %}
<h2 class="menu-label">{% trans "Instance Settings" %}</h2> <h2 class="menu-label">{% trans "Instance Settings" %}</h2>
<ul class="menu-list"> <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> <li>
{% url 'settings-site' as url %} {% url 'settings-site' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Site Settings" %}</a> <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' %} {% extends 'settings/admin_layout.html' %}
{% block title %}{{ server.server_name }}{% endblock %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load markdown %}
{% block title %}{{ server.server_name }}{% endblock %}
{% block header %} {% block header %}
{{ server.server_name }} {{ server.server_name }}
@ -9,65 +10,69 @@
{% 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> {% 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 %} {% 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 %} {% endblock %}
{% block panel %} {% block panel %}
<div class="columns"> <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> <h2 class="title is-4">{% trans "Details" %}</h2>
<dl> <div class="box is-flex-grow-1 content">
<div class="is-flex"> <dl>
<dt>{% trans "Software:" %}</dt> <div class="is-flex">
<dd>{{ server.application_type }}</dd> <dt>{% trans "Software:" %}</dt>
</div> <dd>{{ server.application_type }}</dd>
<div class="is-flex"> </div>
<dt>{% trans "Version:" %}</dt> <div class="is-flex">
<dd>{{ server.application_version }}</dd> <dt>{% trans "Version:" %}</dt>
</div> <dd>{{ server.application_version }}</dd>
<div class="is-flex"> </div>
<dt>{% trans "Status:" %}</dt> <div class="is-flex">
<dd>{{ server.status }}</dd> <dt>{% trans "Status:" %}</dt>
</div> <dd>{{ server.status }}</dd>
</dl> </div>
</dl>
</div>
</section> </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> <h2 class="title is-4">{% trans "Activity" %}</h2>
<dl> <div class="box is-flex-grow-1 content">
<div class="is-flex"> <dl>
<dt>{% trans "Users:" %}</dt> <div class="is-flex">
<dd> <dt>{% trans "Users:" %}</dt>
{{ users.count }} <dd>
{% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %} {{ users.count }}
</dd> {% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
</div> </dd>
<div class="is-flex"> </div>
<dt>{% trans "Reports:" %}</dt> <div class="is-flex">
<dd> <dt>{% trans "Reports:" %}</dt>
{{ reports.count }} <dd>
{% if reports.count %}(<a href="{% url 'settings-reports' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %} {{ reports.count }}
</dd> {% if reports.count %}(<a href="{% url 'settings-reports' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
</div> </dd>
<div class="is-flex"> </div>
<dt>{% trans "Followed by us:" %}</dt> <div class="is-flex">
<dd> <dt>{% trans "Followed by us:" %}</dt>
{{ followed_by_us.count }} <dd>
</dd> {{ followed_by_us.count }}
</div> </dd>
<div class="is-flex"> </div>
<dt>{% trans "Followed by them:" %}</dt> <div class="is-flex">
<dd> <dt>{% trans "Followed by them:" %}</dt>
{{ followed_by_them.count }} <dd>
</dd> {{ followed_by_them.count }}
</div> </dd>
<div class="is-flex"> </div>
<dt>{% trans "Blocked by us:" %}</dt> <div class="is-flex">
<dd> <dt>{% trans "Blocked by us:" %}</dt>
{{ blocked_by_us.count }} <dd>
</dd> {{ blocked_by_us.count }}
</div> </dd>
</dl> </div>
</dl>
</div>
</section> </section>
</div> </div>
@ -78,11 +83,11 @@
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
{% trans "Edit" as button_text %} {% 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> </div>
</header> </header>
{% if server.notes %} {% 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 %} {% endif %}
<form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit-notes"> <form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit-notes">
{% csrf_token %} {% csrf_token %}

View file

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

View file

@ -37,16 +37,16 @@
<section class="block" id="images"> <section class="block" id="images">
<h2 class="title is-4">{% trans "Images" %}</h2> <h2 class="title is-4">{% trans "Images" %}</h2>
<div class="field is-grouped"> <div class="columns">
<div class="control"> <div class="column">
<label class="label" for="id_logo">{% trans "Logo:" %}</label> <label class="label" for="id_logo">{% trans "Logo:" %}</label>
{{ site_form.logo }} {{ site_form.logo }}
</div> </div>
<div class="control"> <div class="column">
<label class="label" for="id_logo_small">{% trans "Logo small:" %}</label> <label class="label" for="id_logo_small">{% trans "Logo small:" %}</label>
{{ site_form.logo_small }} {{ site_form.logo_small }}
</div> </div>
<div class="control"> <div class="column">
<label class="label" for="id_favicon">{% trans "Favicon:" %}</label> <label class="label" for="id_favicon">{% trans "Favicon:" %}</label>
{{ site_form.favicon }} {{ site_form.favicon }}
</div> </div>
@ -69,6 +69,10 @@
<label class="label" for="id_admin_email">{% trans "Admin email:" %}</label> <label class="label" for="id_admin_email">{% trans "Admin email:" %}</label>
{{ site_form.admin_email }} {{ site_form.admin_email }}
</div> </div>
<div class="control">
<label class="label" for="id_footer_item">{% trans "Additional info:" %}</label>
{{ site_form.footer_item }}
</div>
</section> </section>
<hr aria-hidden="true"> <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 %} {% endcomment %}
{% for author in book.authors.all %} {% for author in book.authors.all %}
<a <a
href="/author/{{ author.id }}" href="{{ author.local_path }}"
class="author" class="author"
itemprop="author" itemprop="author"
itemscope 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 }}"> <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 %} {% spaceless %}
{% load bookwyrm_tags %}
{% load i18n %} {% load i18n %}
<div class="cover-container is-{{ size }}"> <figure
{% if book.cover %} class="
<img cover-container
class="book-cover" {{ cover_class }}
src="/images/{{ book.cover }}"
alt="{{ book.alt_text }}"
title="{{ book.alt_text }}"
itemprop="thumbnailUrl"
>
{% else %}
<div class="no-cover book-cover">
<img
class="book-cover"
src="/static/images/no_cover.jpg"
alt="{% trans "No cover" %}"
>
<div> {% if not book.cover %}
<p>{{ book.alt_text }}</p> no-cover
</div> {% endif %}
</div> "
{% if book.alt_text %}
title="{{ book.alt_text }}"
{% endif %} {% endif %}
</div> >
<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 %}
>
{% if not book.cover and book.alt_text %}
<figcaption class="cover_caption">
<p>{{ book.alt_text }}</p>
</figcaption>
{% endif %}
</figure>
{% endspaceless %} {% 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 i18n %}
{% load bookwyrm_tags %} {% load utilities %}
{% if book.authors %} {% 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 %} {% else %}
<a href="{{ book.local_path }}">{{ book|title }}</a> <a href="{{ book.local_path }}">{{ book|book_title }}</a>
{% endif %} {% endif %}

View file

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

View file

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

View file

@ -1,4 +1,7 @@
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load utilities %}
{% load status_display %}
{% load i18n %} {% 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 }}"> <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 %} {% csrf_token %}
@ -36,13 +39,23 @@
<div class="control"> <div class="control">
{% if type == 'quotation' %} {% if type == 'quotation' %}
<textarea name="quote" class="textarea" id="id_quote_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required>{{ draft.quote|default:'' }}</textarea> <textarea
{% elif type == 'reply' %} name="quote"
{% include 'snippets/content_warning_field.html' with parent_status=status %} class="textarea"
<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> id="id_quote_{{ book.id }}_{{ type }}"
placeholder="{{ placeholder }}"
required
>{{ draft.quote|default:'' }}</textarea>
{% else %} {% else %}
{% include 'snippets/content_warning_field.html' with parent_status=status %} {% 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 %} {% endif %}
</div> </div>
</div> </div>
@ -90,7 +103,7 @@
{# bottom bar #} {# bottom bar #}
<input type="checkbox" class="is-hidden" name="sensitive" id="id_show_spoilers-{{ uuid }}" {% if draft.content_warning or status.content_warning %}checked{% endif %} aria-hidden="true"> <input type="checkbox" class="is-hidden" name="sensitive" id="id_show_spoilers-{{ uuid }}" {% if draft.content_warning or status.content_warning %}checked{% endif %} aria-hidden="true">
<div class="columns mt-1"> <div class="columns mt-1">
<div class="field has-addons column"> <div class="field has-addons column">
<div class="control"> <div class="control">

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