forked from mirrors/bookwyrm
Merge branch 'main' into list-style-fixes
This commit is contained in:
commit
1f747e4f68
126 changed files with 5790 additions and 9540 deletions
|
@ -7,6 +7,9 @@ DEBUG=true
|
|||
DOMAIN=your.domain.here
|
||||
#EMAIL=your@email.here
|
||||
|
||||
# Used for deciding which editions to prefer
|
||||
DEFAULT_LANGUAGE="English"
|
||||
|
||||
## Leave unset to allow all hosts
|
||||
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
|
||||
|
||||
|
|
|
@ -7,6 +7,9 @@ DEBUG=false
|
|||
DOMAIN=your.domain.here
|
||||
EMAIL=your@email.here
|
||||
|
||||
# Used for deciding which editions to prefer
|
||||
DEFAULT_LANGUAGE="English"
|
||||
|
||||
## Leave unset to allow all hosts
|
||||
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
|
||||
|
||||
|
|
4
.github/workflows/black.yml
vendored
4
.github/workflows/black.yml
vendored
|
@ -8,6 +8,4 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: psf/black@stable
|
||||
with:
|
||||
args: ". --check -l 80 -S"
|
||||
- uses: psf/black@21.4b2
|
||||
|
|
|
@ -9,10 +9,11 @@ Permission is hereby granted, free of charge, to any person or organization (the
|
|||
1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software.
|
||||
|
||||
2. The User is one of the following:
|
||||
a. An individual person, laboring for themselves
|
||||
b. A non-profit organization
|
||||
c. An educational institution
|
||||
d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor
|
||||
|
||||
1. An individual person, laboring for themselves
|
||||
2. A non-profit organization
|
||||
3. An educational institution
|
||||
4. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor
|
||||
|
||||
3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote.
|
||||
|
|
@ -7,11 +7,22 @@ from .image import Document
|
|||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Book(ActivityObject):
|
||||
class BookData(ActivityObject):
|
||||
"""shared fields for all book data and authors"""
|
||||
|
||||
openlibraryKey: str = None
|
||||
inventaireId: str = None
|
||||
librarythingKey: str = None
|
||||
goodreadsKey: str = None
|
||||
bnfId: str = None
|
||||
lastEditedBy: str = None
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Book(BookData):
|
||||
"""serializes an edition or work, abstract"""
|
||||
|
||||
title: str
|
||||
lastEditedBy: str = None
|
||||
sortTitle: str = ""
|
||||
subtitle: str = ""
|
||||
description: str = ""
|
||||
|
@ -25,10 +36,6 @@ class Book(ActivityObject):
|
|||
firstPublishedDate: str = ""
|
||||
publishedDate: str = ""
|
||||
|
||||
openlibraryKey: str = ""
|
||||
librarythingKey: str = ""
|
||||
goodreadsKey: str = ""
|
||||
|
||||
cover: Document = None
|
||||
type: str = "Book"
|
||||
|
||||
|
@ -55,23 +62,21 @@ class Work(Book):
|
|||
"""work instance of a book object"""
|
||||
|
||||
lccn: str = ""
|
||||
defaultEdition: str = ""
|
||||
editions: List[str] = field(default_factory=lambda: [])
|
||||
type: str = "Work"
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
class Author(ActivityObject):
|
||||
class Author(BookData):
|
||||
"""author of a book"""
|
||||
|
||||
name: str
|
||||
lastEditedBy: str = None
|
||||
isni: str = None
|
||||
viafId: str = None
|
||||
gutenbergId: str = None
|
||||
born: str = None
|
||||
died: str = None
|
||||
aliases: List[str] = field(default_factory=lambda: [])
|
||||
bio: str = ""
|
||||
openlibraryKey: str = ""
|
||||
librarythingKey: str = ""
|
||||
goodreadsKey: str = ""
|
||||
wikipediaLink: str = ""
|
||||
type: str = "Author"
|
||||
|
|
|
@ -83,4 +83,5 @@ class Rating(Comment):
|
|||
|
||||
rating: int
|
||||
content: str = None
|
||||
name: str = None # not used, but the model inherits from Review
|
||||
type: str = "Rating"
|
||||
|
|
|
@ -44,7 +44,7 @@ class AbstractMinimalConnector(ABC):
|
|||
if min_confidence:
|
||||
params["min_confidence"] = min_confidence
|
||||
|
||||
data = get_data(
|
||||
data = self.get_search_data(
|
||||
"%s%s" % (self.search_url, query),
|
||||
params=params,
|
||||
)
|
||||
|
@ -57,7 +57,7 @@ class AbstractMinimalConnector(ABC):
|
|||
def isbn_search(self, query):
|
||||
"""isbn search"""
|
||||
params = {}
|
||||
data = get_data(
|
||||
data = self.get_search_data(
|
||||
"%s%s" % (self.isbn_search_url, query),
|
||||
params=params,
|
||||
)
|
||||
|
@ -68,6 +68,10 @@ class AbstractMinimalConnector(ABC):
|
|||
results.append(self.format_isbn_search_result(doc))
|
||||
return results
|
||||
|
||||
def get_search_data(self, remote_id, **kwargs): # pylint: disable=no-self-use
|
||||
"""this allows connectors to override the default behavior"""
|
||||
return get_data(remote_id, **kwargs)
|
||||
|
||||
@abstractmethod
|
||||
def get_or_create_book(self, remote_id):
|
||||
"""pull up a book record by whatever means possible"""
|
||||
|
@ -112,13 +116,12 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
remote_id
|
||||
) or models.Work.find_existing_by_remote_id(remote_id)
|
||||
if existing:
|
||||
if hasattr(existing, "get_default_editon"):
|
||||
return existing.get_default_editon()
|
||||
if hasattr(existing, "default_edition"):
|
||||
return existing.default_edition
|
||||
return existing
|
||||
|
||||
# load the json
|
||||
data = get_data(remote_id)
|
||||
mapped_data = dict_from_mappings(data, self.book_mappings)
|
||||
data = self.get_book_data(remote_id)
|
||||
if self.is_work_data(data):
|
||||
try:
|
||||
edition_data = self.get_edition_from_work_data(data)
|
||||
|
@ -126,30 +129,36 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
# hack: re-use the work data as the edition data
|
||||
# this is why remote ids aren't necessarily unique
|
||||
edition_data = data
|
||||
work_data = mapped_data
|
||||
work_data = data
|
||||
else:
|
||||
edition_data = data
|
||||
try:
|
||||
work_data = self.get_work_from_edition_data(data)
|
||||
work_data = dict_from_mappings(work_data, self.book_mappings)
|
||||
except (KeyError, ConnectorException):
|
||||
work_data = mapped_data
|
||||
edition_data = data
|
||||
except (KeyError, ConnectorException) as e:
|
||||
logger.exception(e)
|
||||
work_data = data
|
||||
|
||||
if not work_data or not edition_data:
|
||||
raise ConnectorException("Unable to load book data: %s" % remote_id)
|
||||
|
||||
with transaction.atomic():
|
||||
# create activitypub object
|
||||
work_activity = activitypub.Work(**work_data)
|
||||
work_activity = activitypub.Work(
|
||||
**dict_from_mappings(work_data, self.book_mappings)
|
||||
)
|
||||
# this will dedupe automatically
|
||||
work = work_activity.to_model(model=models.Work)
|
||||
for author in self.get_authors_from_data(data):
|
||||
for author in self.get_authors_from_data(work_data):
|
||||
work.authors.add(author)
|
||||
|
||||
edition = self.create_edition_from_data(work, edition_data)
|
||||
load_more_data.delay(self.connector.id, work.id)
|
||||
return edition
|
||||
|
||||
def get_book_data(self, remote_id): # pylint: disable=no-self-use
|
||||
"""this allows connectors to override the default behavior"""
|
||||
return get_data(remote_id)
|
||||
|
||||
def create_edition_from_data(self, work, edition_data):
|
||||
"""if we already have the work, we're ready"""
|
||||
mapped_data = dict_from_mappings(edition_data, self.book_mappings)
|
||||
|
@ -159,10 +168,6 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
edition.connector = self.connector
|
||||
edition.save()
|
||||
|
||||
if not work.default_edition:
|
||||
work.default_edition = edition
|
||||
work.save()
|
||||
|
||||
for author in self.get_authors_from_data(edition_data):
|
||||
edition.authors.add(author)
|
||||
if not edition.authors.exists() and work.authors.exists():
|
||||
|
@ -176,7 +181,7 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
if existing:
|
||||
return existing
|
||||
|
||||
data = get_data(remote_id)
|
||||
data = self.get_book_data(remote_id)
|
||||
|
||||
mapped_data = dict_from_mappings(data, self.author_mappings)
|
||||
try:
|
||||
|
@ -213,6 +218,10 @@ def dict_from_mappings(data, mappings):
|
|||
the subclass"""
|
||||
result = {}
|
||||
for mapping in mappings:
|
||||
# sometimes there are multiple mappings for one field, don't
|
||||
# overwrite earlier writes in that case
|
||||
if mapping.local_field in result and result[mapping.local_field]:
|
||||
continue
|
||||
result[mapping.local_field] = mapping.get_value(data)
|
||||
return result
|
||||
|
||||
|
@ -273,6 +282,7 @@ class SearchResult:
|
|||
title: str
|
||||
key: str
|
||||
connector: object
|
||||
view_link: str = None
|
||||
author: str = None
|
||||
year: str = None
|
||||
cover: str = None
|
||||
|
|
|
@ -7,11 +7,7 @@ class Connector(AbstractMinimalConnector):
|
|||
"""this is basically just for search"""
|
||||
|
||||
def get_or_create_book(self, remote_id):
|
||||
edition = activitypub.resolve_remote_id(remote_id, model=models.Edition)
|
||||
work = edition.parent_work
|
||||
work.default_edition = work.get_default_edition()
|
||||
work.save()
|
||||
return edition
|
||||
return activitypub.resolve_remote_id(remote_id, model=models.Edition)
|
||||
|
||||
def parse_search_data(self, data):
|
||||
return data
|
||||
|
|
|
@ -29,8 +29,6 @@ def search(query, min_confidence=0.1):
|
|||
isbn = re.sub(r"[\W_]", "", query)
|
||||
maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13
|
||||
|
||||
dedup_slug = lambda r: "%s/%s/%s" % (r.title, r.author, r.year)
|
||||
result_index = set()
|
||||
for connector in get_connectors():
|
||||
result_set = None
|
||||
if maybe_isbn:
|
||||
|
@ -53,10 +51,6 @@ def search(query, min_confidence=0.1):
|
|||
logger.exception(e)
|
||||
continue
|
||||
|
||||
# if the search results look the same, ignore them
|
||||
result_set = [r for r in result_set if dedup_slug(r) not in result_index]
|
||||
# `|=` concats two sets. WE ARE GETTING FANCY HERE
|
||||
result_index |= set(dedup_slug(r) for r in result_set)
|
||||
results.append(
|
||||
{
|
||||
"connector": connector,
|
||||
|
|
218
bookwyrm/connectors/inventaire.py
Normal file
218
bookwyrm/connectors/inventaire.py
Normal file
|
@ -0,0 +1,218 @@
|
|||
""" 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 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
|
||||
)
|
||||
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,
|
||||
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}".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
|
|
@ -14,8 +14,8 @@ class Connector(AbstractConnector):
|
|||
def __init__(self, identifier):
|
||||
super().__init__(identifier)
|
||||
|
||||
get_first = lambda a: a[0]
|
||||
get_remote_id = lambda a: self.base_url + a
|
||||
get_first = lambda a, *args: a[0]
|
||||
get_remote_id = lambda a, *args: self.base_url + a
|
||||
self.book_mappings = [
|
||||
Mapping("title"),
|
||||
Mapping("id", remote_field="key", formatter=get_remote_id),
|
||||
|
|
|
@ -3,7 +3,7 @@ from functools import reduce
|
|||
import operator
|
||||
|
||||
from django.contrib.postgres.search import SearchRank, SearchVector
|
||||
from django.db.models import Count, F, Q
|
||||
from django.db.models import Count, OuterRef, Subquery, F, Q
|
||||
|
||||
from bookwyrm import models
|
||||
from .abstract_connector import AbstractConnector, SearchResult
|
||||
|
@ -47,7 +47,16 @@ class Connector(AbstractConnector):
|
|||
|
||||
# when there are multiple editions of the same work, pick the default.
|
||||
# it would be odd for this to happen.
|
||||
results = results.filter(parent_work__default_edition__id=F("id")) or results
|
||||
|
||||
default_editions = models.Edition.objects.filter(
|
||||
parent_work=OuterRef("parent_work")
|
||||
).order_by("-edition_rank")
|
||||
results = (
|
||||
results.annotate(
|
||||
default_id=Subquery(default_editions.values("id")[:1])
|
||||
).filter(default_id=F("id"))
|
||||
or results
|
||||
)
|
||||
|
||||
search_results = []
|
||||
for result in results:
|
||||
|
@ -60,6 +69,10 @@ class Connector(AbstractConnector):
|
|||
return search_results
|
||||
|
||||
def format_search_result(self, search_result):
|
||||
cover = None
|
||||
if search_result.cover:
|
||||
cover = "%s%s" % (self.covers_url, search_result.cover)
|
||||
|
||||
return SearchResult(
|
||||
title=search_result.title,
|
||||
key=search_result.remote_id,
|
||||
|
@ -68,7 +81,7 @@ class Connector(AbstractConnector):
|
|||
if search_result.published_date
|
||||
else None,
|
||||
connector=self,
|
||||
cover="%s%s" % (self.covers_url, search_result.cover),
|
||||
cover=cover,
|
||||
confidence=search_result.rank if hasattr(search_result, "rank") else 1,
|
||||
)
|
||||
|
||||
|
@ -112,7 +125,15 @@ def search_identifiers(query, *filters):
|
|||
|
||||
# when there are multiple editions of the same work, pick the default.
|
||||
# it would be odd for this to happen.
|
||||
return results.filter(parent_work__default_edition__id=F("id")) or results
|
||||
default_editions = models.Edition.objects.filter(
|
||||
parent_work=OuterRef("parent_work")
|
||||
).order_by("-edition_rank")
|
||||
return (
|
||||
results.annotate(default_id=Subquery(default_editions.values("id")[:1])).filter(
|
||||
default_id=F("id")
|
||||
)
|
||||
or results
|
||||
)
|
||||
|
||||
|
||||
def search_title_author(query, min_confidence, *filters):
|
||||
|
@ -140,10 +161,10 @@ def search_title_author(query, min_confidence, *filters):
|
|||
|
||||
for work_id in set(editions_of_work):
|
||||
editions = results.filter(parent_work=work_id)
|
||||
default = editions.filter(parent_work__default_edition=F("id"))
|
||||
default_rank = default.first().rank if default.exists() else 0
|
||||
default = editions.order_by("-edition_rank").first()
|
||||
default_rank = default.rank if default else 0
|
||||
# if mutliple books have the top rank, pick the default edition
|
||||
if default_rank == editions.first().rank:
|
||||
yield default.first()
|
||||
yield default
|
||||
else:
|
||||
yield editions.first()
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
""" settings book data connectors """
|
||||
|
||||
CONNECTORS = ["openlibrary", "self_connector", "bookwyrm_connector"]
|
||||
CONNECTORS = ["openlibrary", "inventaire", "self_connector", "bookwyrm_connector"]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django.core.management.base import BaseCommand, CommandError
|
||||
""" What you need in the database to make it work """
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
|
@ -7,12 +8,14 @@ from bookwyrm.settings import DOMAIN
|
|||
|
||||
|
||||
def init_groups():
|
||||
"""permission levels"""
|
||||
groups = ["admin", "moderator", "editor"]
|
||||
for group in groups:
|
||||
Group.objects.create(name=group)
|
||||
|
||||
|
||||
def init_permissions():
|
||||
"""permission types"""
|
||||
permissions = [
|
||||
{
|
||||
"codename": "edit_instance_settings",
|
||||
|
@ -69,6 +72,7 @@ def init_permissions():
|
|||
|
||||
|
||||
def init_connectors():
|
||||
"""access book data sources"""
|
||||
Connector.objects.create(
|
||||
identifier=DOMAIN,
|
||||
name="Local",
|
||||
|
@ -94,6 +98,18 @@ def init_connectors():
|
|||
priority=2,
|
||||
)
|
||||
|
||||
Connector.objects.create(
|
||||
identifier="inventaire.io",
|
||||
name="Inventaire",
|
||||
connector_file="inventaire",
|
||||
base_url="https://inventaire.io",
|
||||
books_url="https://inventaire.io/api/entities",
|
||||
covers_url="https://inventaire.io",
|
||||
search_url="https://inventaire.io/api/search?types=works&types=works&search=",
|
||||
isbn_search_url="https://inventaire.io/api/entities?action=by-uris&uris=isbn%3A",
|
||||
priority=3,
|
||||
)
|
||||
|
||||
Connector.objects.create(
|
||||
identifier="openlibrary.org",
|
||||
name="OpenLibrary",
|
||||
|
@ -118,7 +134,11 @@ def init_federated_servers():
|
|||
|
||||
|
||||
def init_settings():
|
||||
SiteSettings.objects.create()
|
||||
"""info about the instance"""
|
||||
SiteSettings.objects.create(
|
||||
support_link="https://www.patreon.com/bookwyrm",
|
||||
support_title="Patreon",
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
|
30
bookwyrm/migrations/0062_auto_20210406_1731.py
Normal file
30
bookwyrm/migrations/0062_auto_20210406_1731.py
Normal 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,
|
||||
),
|
||||
),
|
||||
]
|
63
bookwyrm/migrations/0063_auto_20210407_0045.py
Normal file
63
bookwyrm/migrations/0063_auto_20210407_0045.py
Normal 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
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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 = []
|
17
bookwyrm/migrations/0072_remove_work_default_edition.py
Normal file
17
bookwyrm/migrations/0072_remove_work_default_edition.py
Normal 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",
|
||||
),
|
||||
]
|
18
bookwyrm/migrations/0073_sitesettings_footer_item.py
Normal file
18
bookwyrm/migrations/0073_sitesettings_footer_item.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -14,6 +14,15 @@ class Author(BookDataModel):
|
|||
wikipedia_link = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
isni = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
viaf_id = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
gutenberg_id = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
# idk probably other keys would be useful here?
|
||||
born = fields.DateTimeField(blank=True, null=True)
|
||||
died = fields.DateTimeField(blank=True, null=True)
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
""" database schema for books and shelves """
|
||||
import re
|
||||
|
||||
from django.db import models, transaction
|
||||
from django.db import models
|
||||
from model_utils.managers import InheritanceManager
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from bookwyrm.settings import DOMAIN, DEFAULT_LANGUAGE
|
||||
|
||||
from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
|
||||
from .base_model import BookWyrmModel
|
||||
|
@ -19,12 +19,18 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
|
|||
openlibrary_key = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
inventaire_id = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
librarything_key = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
goodreads_key = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
bnf_id = fields.CharField( # Bibliothèque nationale de France
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
|
||||
last_edited_by = fields.ForeignKey(
|
||||
"User",
|
||||
|
@ -137,10 +143,6 @@ class Work(OrderedCollectionPageMixin, Book):
|
|||
lccn = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
# this has to be nullable but should never be null
|
||||
default_edition = fields.ForeignKey(
|
||||
"Edition", on_delete=models.PROTECT, null=True, load_remote=False
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""set some fields on the edition object"""
|
||||
|
@ -149,18 +151,10 @@ class Work(OrderedCollectionPageMixin, Book):
|
|||
edition.save()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def get_default_edition(self):
|
||||
@property
|
||||
def default_edition(self):
|
||||
"""in case the default edition is not set"""
|
||||
return self.default_edition or self.editions.order_by("-edition_rank").first()
|
||||
|
||||
@transaction.atomic()
|
||||
def reset_default_edition(self):
|
||||
"""sets a new default edition based on computed rank"""
|
||||
self.default_edition = None
|
||||
# editions are re-ranked implicitly
|
||||
self.save()
|
||||
self.default_edition = self.get_default_edition()
|
||||
self.save()
|
||||
return self.editions.order_by("-edition_rank").first()
|
||||
|
||||
def to_edition_list(self, **kwargs):
|
||||
"""an ordered collection of editions"""
|
||||
|
@ -214,17 +208,20 @@ class Edition(Book):
|
|||
activity_serializer = activitypub.Edition
|
||||
name_field = "title"
|
||||
|
||||
def get_rank(self, ignore_default=False):
|
||||
def get_rank(self):
|
||||
"""calculate how complete the data is on this edition"""
|
||||
if (
|
||||
not ignore_default
|
||||
and self.parent_work
|
||||
and self.parent_work.default_edition == self
|
||||
):
|
||||
# default edition has the highest rank
|
||||
return 20
|
||||
rank = 0
|
||||
# big ups for havinga cover
|
||||
rank += int(bool(self.cover)) * 3
|
||||
# is it in the instance's preferred language?
|
||||
rank += int(bool(DEFAULT_LANGUAGE in self.languages))
|
||||
# prefer print editions
|
||||
if self.physical_format:
|
||||
rank += int(
|
||||
bool(self.physical_format.lower() in ["paperback", "hardcover"])
|
||||
)
|
||||
|
||||
# does it have metadata?
|
||||
rank += int(bool(self.isbn_13))
|
||||
rank += int(bool(self.isbn_10))
|
||||
rank += int(bool(self.oclc_number))
|
||||
|
@ -242,6 +239,12 @@ class Edition(Book):
|
|||
if self.isbn_10 and not self.isbn_13:
|
||||
self.isbn_13 = isbn_10_to_13(self.isbn_10)
|
||||
|
||||
# normalize isbn format
|
||||
if self.isbn_10:
|
||||
self.isbn_10 = re.sub(r"[^0-9X]", "", self.isbn_10)
|
||||
if self.isbn_13:
|
||||
self.isbn_13 = re.sub(r"[^0-9X]", "", self.isbn_13)
|
||||
|
||||
# set rank
|
||||
self.edition_rank = self.get_rank()
|
||||
|
||||
|
|
|
@ -31,16 +31,6 @@ class Connector(BookWyrmModel):
|
|||
# when to reset the query count back to 0 (ie, after 1 day)
|
||||
query_count_expiry = models.DateTimeField(auto_now_add=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
"""check that there's code to actually use this connector"""
|
||||
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
check=models.Q(connector_file__in=ConnectorFiles),
|
||||
name="connector_file_valid",
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return "{} ({})".format(
|
||||
self.identifier,
|
||||
|
|
|
@ -19,19 +19,28 @@ class SiteSettings(models.Model):
|
|||
max_length=150, default="Social Reading and Reviewing"
|
||||
)
|
||||
instance_description = models.TextField(default="This instance has no description.")
|
||||
|
||||
# about page
|
||||
registration_closed_text = models.TextField(
|
||||
default="Contact an administrator to get an invite"
|
||||
)
|
||||
code_of_conduct = models.TextField(default="Add a code of conduct here.")
|
||||
privacy_policy = models.TextField(default="Add a privacy policy here.")
|
||||
|
||||
# registration
|
||||
allow_registration = models.BooleanField(default=True)
|
||||
allow_invite_requests = models.BooleanField(default=True)
|
||||
|
||||
# images
|
||||
logo = models.ImageField(upload_to="logos/", null=True, blank=True)
|
||||
logo_small = models.ImageField(upload_to="logos/", null=True, blank=True)
|
||||
favicon = models.ImageField(upload_to="logos/", null=True, blank=True)
|
||||
|
||||
# footer
|
||||
support_link = models.CharField(max_length=255, null=True, blank=True)
|
||||
support_title = models.CharField(max_length=100, null=True, blank=True)
|
||||
admin_email = models.EmailField(max_length=255, null=True, blank=True)
|
||||
footer_item = models.TextField(null=True, blank=True)
|
||||
|
||||
@classmethod
|
||||
def get(cls):
|
||||
|
|
|
@ -150,6 +150,19 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
"""for consistent naming"""
|
||||
return not self.is_active
|
||||
|
||||
@property
|
||||
def unread_notification_count(self):
|
||||
"""count of notifications, for the templates"""
|
||||
return self.notification_set.filter(read=False).count()
|
||||
|
||||
@property
|
||||
def has_unread_mentions(self):
|
||||
"""whether any of the unread notifications are conversations"""
|
||||
return self.notification_set.filter(
|
||||
read=False,
|
||||
notification_type__in=["REPLY", "MENTION", "TAG", "REPORT"],
|
||||
).exists()
|
||||
|
||||
activity_serializer = activitypub.Person
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -11,6 +11,7 @@ DOMAIN = env("DOMAIN")
|
|||
VERSION = "0.0.1"
|
||||
|
||||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||
|
||||
# celery
|
||||
CELERY_BROKER = env("CELERY_BROKER")
|
||||
|
|
|
@ -140,7 +140,7 @@ body {
|
|||
*
|
||||
* \e9d9: filled star
|
||||
* \e9d7: empty star;
|
||||
******************************************************************************/
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.form-rate-stars {
|
||||
width: max-content;
|
||||
|
@ -166,70 +166,67 @@ body {
|
|||
}
|
||||
|
||||
/** Book covers
|
||||
*
|
||||
* - .is-cover gives the behaviour of the cover and its surrounding. (optional)
|
||||
* - .cover-container gives the dimensions and position (for borders, image and other elements).
|
||||
* - .book-cover is positioned and sized based on its container.
|
||||
*
|
||||
* To have the cover within specific dimensions, specify a width or height for
|
||||
* standard bulma’s named breapoints:
|
||||
*
|
||||
* `is-(w|h)-(auto|xs|s|m|l|xl|xxl)[-(mobile|tablet|desktop)]`
|
||||
*
|
||||
* The cover will be centered horizontally and vertically within those dimensions.
|
||||
*
|
||||
* When using `.column.is-N`, add `.is-w-auto` to the container so that the flex
|
||||
* calculations are not biased by the default `max-content`.
|
||||
******************************************************************************/
|
||||
|
||||
.column.is-cover {
|
||||
flex-grow: 0 !important;
|
||||
}
|
||||
|
||||
.column.is-cover,
|
||||
.column.is-cover + .column {
|
||||
flex-basis: auto !important;
|
||||
}
|
||||
|
||||
.cover-container {
|
||||
height: 250px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: max-content;
|
||||
max-width: 250px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover-container.is-large {
|
||||
height: max-content;
|
||||
max-width: 330px;
|
||||
}
|
||||
|
||||
.cover-container.is-large img {
|
||||
max-height: 500px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.cover-container.is-medium {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.cover-container.is-small {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.cover-container {
|
||||
height: 200px;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.cover-container.is-medium {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
/* Book cover
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.book-cover {
|
||||
height: 100%;
|
||||
object-fit: scale-down;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
/* Useful when stretching under-sized images. */
|
||||
image-rendering: optimizeQuality;
|
||||
image-rendering: smooth;
|
||||
}
|
||||
|
||||
.no-cover {
|
||||
position: relative;
|
||||
white-space: normal;
|
||||
}
|
||||
/* Cover caption
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.no-cover div {
|
||||
.no-cover .cover_caption {
|
||||
position: absolute;
|
||||
padding: 1em;
|
||||
color: white;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cover-container.is-medium .no-cover div {
|
||||
font-size: 0.9em;
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
.cover-container.is-small .no-cover div {
|
||||
font-size: 0.7em;
|
||||
padding: 0.1em;
|
||||
padding: 0.25em;
|
||||
font-size: 0.75em;
|
||||
color: white;
|
||||
background-color: #002549;
|
||||
}
|
||||
|
||||
/** Avatars
|
||||
|
@ -240,16 +237,6 @@ body {
|
|||
display: inline;
|
||||
}
|
||||
|
||||
.is-32x32 {
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.is-96x96 {
|
||||
min-width: 96px;
|
||||
min-height: 96px;
|
||||
}
|
||||
|
||||
/** Statuses: Quotes
|
||||
*
|
||||
* \e906: icon-quote-open
|
||||
|
@ -397,3 +384,386 @@ ol.ordered-list li::before {
|
|||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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 Bulma’s. They follow the same conventions.
|
||||
* Add those you’ll 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;
|
||||
}
|
||||
}
|
||||
|
|
2
bookwyrm/static/css/vendor/bulma.min.css
vendored
2
bookwyrm/static/css/vendor/bulma.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -97,10 +97,12 @@ let BookWyrm = new class {
|
|||
updateCountElement(counter, data) {
|
||||
const currentCount = counter.innerText;
|
||||
const count = data.count;
|
||||
const hasMentions = data.has_mentions;
|
||||
|
||||
if (count != currentCount) {
|
||||
this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-hidden', count < 1);
|
||||
counter.innerText = count;
|
||||
this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-danger', hasMentions);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,3 +39,6 @@
|
|||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% include 'snippets/datepicker_js.html' %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -48,10 +48,9 @@
|
|||
|
||||
<div class="columns">
|
||||
<div class="column is-one-fifth">
|
||||
<div class="is-clipped">
|
||||
{% include 'snippets/book_cover.html' with book=book size=large %}
|
||||
{% include 'snippets/rate_action.html' with user=request.user book=book %}
|
||||
</div>
|
||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m-mobile' %}
|
||||
{% include 'snippets/rate_action.html' with user=request.user book=book %}
|
||||
|
||||
<div class="mb-3">
|
||||
{% include 'snippets/shelve_button/shelve_button.html' %}
|
||||
</div>
|
||||
|
@ -81,6 +80,9 @@
|
|||
{% if book.openlibrary_key %}
|
||||
<p><a href="https://openlibrary.org/books/{{ book.openlibrary_key }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a></p>
|
||||
{% endif %}
|
||||
{% if book.inventaire_id %}
|
||||
<p><a href="https://inventaire.io/entity/{{ book.inventaire_id }}" target="_blank" rel="noopener">{% trans "View on Inventaire" %}</a></p>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
@ -153,9 +155,12 @@
|
|||
</div>
|
||||
|
||||
{% if user_authenticated %}
|
||||
<hr aria-hidden="true">
|
||||
<section class="block">
|
||||
<header class="columns">
|
||||
<h2 class="column title is-5 mb-1">{% trans "Your reading activity" %}</h2>
|
||||
<div class="column">
|
||||
<h2 class="title is-5">{% trans "Your reading activity" %}</h2>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
{% trans "Add read dates" as button_text %}
|
||||
{% include 'snippets/toggle/open_button.html' with text=button_text icon="plus" class="is-small" controls_text="add-readthrough" %}
|
||||
|
@ -182,11 +187,88 @@
|
|||
{% include 'snippets/readthrough.html' with readthrough=readthrough %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
<hr aria-hidden="true">
|
||||
|
||||
<section class="box">
|
||||
{% include 'snippets/create_status.html' with book=book hide_cover=True %}
|
||||
</section>
|
||||
{% endif %}
|
||||
<div class="block" id="reviews">
|
||||
{% if request.user.is_authenticated %}
|
||||
{% if user_statuses.review_count or user_statuses.comment_count or user_stuatses.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 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 class="column is-one-fifth">
|
||||
{% if book.subjects %}
|
||||
|
@ -245,84 +327,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block" id="reviews">
|
||||
{% if request.user.is_authenticated %}
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
{% url 'book' book.id as tab_url %}
|
||||
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||
<a href="{{ tab_url }}">{% trans "Reviews" %} ({{ review_count }})</a>
|
||||
</li>
|
||||
{% if user_statuses.review_count %}
|
||||
{% url 'book-user-statuses' book.id 'review' as tab_url %}
|
||||
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||
<a href="{{ tab_url }}">{% trans "Your reviews" %} ({{ user_statuses.review_count }})</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if user_statuses.comment_count %}
|
||||
{% url 'book-user-statuses' book.id 'comment' as tab_url %}
|
||||
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||
<a href="{{ tab_url }}">{% trans "Your comments" %} ({{ user_statuses.comment_count }})</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if user_statuses.quotation_count %}
|
||||
{% url 'book-user-statuses' book.id 'quote' as tab_url %}
|
||||
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||
<a href="{{ tab_url }}">{% trans "Your quotes" %} ({{ user_statuses.quotation_count }})</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% for review in statuses %}
|
||||
<div
|
||||
class="block"
|
||||
itemprop="review"
|
||||
itemscope
|
||||
itemtype="https://schema.org/Review"
|
||||
>
|
||||
{% with status=review hide_book=True depth=1 %}
|
||||
{% include 'snippets/status/status.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="block is-flex is-flex-wrap-wrap">
|
||||
{% for rating in ratings %}
|
||||
{% with user=rating.user %}
|
||||
<div class="block mr-5">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
{% include 'snippets/avatar.html' %}
|
||||
</div>
|
||||
|
||||
<div class="media-content">
|
||||
<div>
|
||||
<a href="{{ user.local_path }}">{{ user.display_name }}</a>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<p class="mr-1">{% trans "rated it" %}</p>
|
||||
|
||||
{% include 'snippets/stars.html' with rating=rating.rating %}
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ rating.remote_id }}">{{ rating.published_date | naturaltime }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="block">
|
||||
{% include 'snippets/pagination.html' with page=statuses path=request.path anchor="#reviews" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/vendor/tabs.js"></script>
|
||||
{% include 'snippets/datepicker_js.html' %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -133,7 +133,11 @@
|
|||
|
||||
<p class="mb-2">
|
||||
<label class="label" for="id_first_published_date">{% trans "First published date:" %}</label>
|
||||
<input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if form.first_published_date.value %} value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %}>
|
||||
<duet-date-picker
|
||||
identifier="id_first_published_date"
|
||||
name="first_published_date"
|
||||
{% if form.first_published_date.value %}value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %}
|
||||
></duet-date-picker>
|
||||
</p>
|
||||
{% for error in form.first_published_date.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
|
@ -141,7 +145,11 @@
|
|||
|
||||
<p class="mb-2">
|
||||
<label class="label" for="id_published_date">{% trans "Published date:" %}</label>
|
||||
<input type="date" name="published_date" class="input" id="id_published_date"{% if form.published_date.value %} value="{{ form.published_date.value|date:'Y-m-d'}}"{% endif %}>
|
||||
<duet-date-picker
|
||||
identifier="id_published_date"
|
||||
name="published_date"
|
||||
{% if form.published_date.value %}value="{{ form.published_date.value|date:'Y-m-d' }}"{% endif %}
|
||||
></duet-date-picker>
|
||||
</p>
|
||||
{% for error in form.published_date.errors %}
|
||||
<p class="help is-danger">{{ error | escape }}</p>
|
||||
|
@ -169,10 +177,11 @@
|
|||
<div class="column is-half">
|
||||
<h2 class="title is-4">{% trans "Cover" %}</h2>
|
||||
<div class="columns">
|
||||
<div class="column is-narrow">
|
||||
{% include 'snippets/book_cover.html' with book=book size="small" %}
|
||||
<div class="column is-3 is-cover">
|
||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-xl-mobile is-w-auto-tablet' %}
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
|
||||
<div class="column">
|
||||
<div class="block">
|
||||
<p>
|
||||
<label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
|
||||
|
@ -238,9 +247,13 @@
|
|||
{% if not confirm_mode %}
|
||||
<div class="block">
|
||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||
<a class="button" href="/book/{{ book.id }}">{% trans "Cancel" %}</a>
|
||||
<a class="button" href="{{ book.local_path}}">{% trans "Cancel" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% include 'snippets/datepicker_js.html' %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -13,32 +13,34 @@
|
|||
|
||||
<div class="block">
|
||||
{% for book in editions %}
|
||||
<div class="columns">
|
||||
<div class="column is-2">
|
||||
<a href="/book/{{ book.id }}">
|
||||
{% include 'snippets/book_cover.html' with book=book size="medium" %}
|
||||
<div class="columns is-gapless mb-6">
|
||||
<div class="column is-cover">
|
||||
<a href="{{ book.local_path }}">
|
||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-m is-h-m align to-l-mobile' %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="column is-7">
|
||||
<h2 class="title is-5">
|
||||
<a href="/book/{{ book.id }}" class="has-text-black">
|
||||
|
||||
<div class="column my-3-mobile ml-3-tablet mr-auto">
|
||||
<h2 class="title is-5 mb-1">
|
||||
<a href="{{ book.local_path }}" class="has-text-black">
|
||||
{{ book.title }}
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
{% with book=book %}
|
||||
<div class="columns is-multiline">
|
||||
<div class="columns is-multiline is-gapless ml-3-tablet">
|
||||
<div class="column is-half">
|
||||
{% include 'book/publisher_info.html' %}
|
||||
</div>
|
||||
|
||||
<div class="column is-half ">
|
||||
<div class="column ml-3-tablet">
|
||||
{% include 'book/book_identifiers.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
|
||||
<div class="column is-narrow">
|
||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book switch_mode=True %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -49,3 +51,7 @@
|
|||
{% include 'snippets/pagination.html' with page=editions path=request.path %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% include 'snippets/datepicker_js.html' %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -11,14 +11,15 @@
|
|||
{% with 0|uuid as uuid %}
|
||||
<div class="box columns">
|
||||
{% if book %}
|
||||
<div class="column is-one-third">
|
||||
<div class="column is-3 is-cover">
|
||||
<div class="block">
|
||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
|
||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto-tablet is-h-l-mobile' %}</a>
|
||||
</div>
|
||||
|
||||
<h3 class="title is-6">{% include 'snippets/book_titleby.html' with book=book %}</h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="column is-two-thirds">
|
||||
<div class="column">
|
||||
{% if draft.reply_parent %}
|
||||
{% include 'snippets/status/status.html' with status=draft.reply_parent no_interact=True %}
|
||||
{% endif %}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block title %}{% trans "Directory" %}{% endblock %}
|
||||
|
||||
|
@ -41,59 +39,7 @@
|
|||
<div class="columns is-multiline">
|
||||
{% for user in users %}
|
||||
<div class="column is-one-third">
|
||||
<div class="card is-stretchable">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<a href="{{ user.local_path }}" class="media-left">
|
||||
{% include 'snippets/avatar.html' with user=user large=True %}
|
||||
</a>
|
||||
<div class="media-content">
|
||||
<a href="{{ user.local_path }}" class="is-block mb-2">
|
||||
<span class="title is-4 is-block">{{ user.display_name }}</span>
|
||||
<span class="subtitle is-7 is-block">@{{ user|username }}</span>
|
||||
</a>
|
||||
{% include 'snippets/follow_button.html' with user=user %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{% if user.summary %}
|
||||
{{ user.summary | to_markdown | safe | truncatechars_html:40 }}
|
||||
{% else %} {% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
{% if user != request.user %}
|
||||
{% if user.mutuals %}
|
||||
<div class="card-footer-item">
|
||||
<div class="has-text-centered">
|
||||
<p class="title is-6 mb-0">{{ user.mutuals }}</p>
|
||||
<p class="help">{% blocktrans count counter=user.mutuals %}follower you follow{% plural %}followers you follow{% endblocktrans %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elif user.shared_books %}
|
||||
<div class="card-footer-item">
|
||||
<div class="has-text-centered">
|
||||
<p class="title is-6 mb-0">{{ user.shared_books }}</p>
|
||||
<p class="help">{% blocktrans count counter=user.shared_books %}book on your shelves{% plural %}books on your shelves{% endblocktrans %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="card-footer-item">
|
||||
<div class="has-text-centered">
|
||||
<p class="title is-6 mb-0">{{ user.status_set.count|intword }}</p>
|
||||
<p class="help">{% trans "posts" %}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
<div class="has-text-centered">
|
||||
<p class="title is-6 mb-0">{{ user.last_active_date|naturalday }}</p>
|
||||
<p class="help">{% trans "last active" %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
{% include 'directory/user_card.html' %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
57
bookwyrm/templates/directory/user_card.html
Normal file
57
bookwyrm/templates/directory/user_card.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% 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 %} {% 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>
|
|
@ -1,19 +1,38 @@
|
|||
|
||||
{% load bookwyrm_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if book %}
|
||||
<div class="columns">
|
||||
<div class="column is-narrow">
|
||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="large" %}</a>
|
||||
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
|
||||
</div>
|
||||
<div class="column">
|
||||
<h3 class="title is-5"><a href="/book/{{ book.id }}">{{ book.title }}</a></h3>
|
||||
{% if book.authors %}
|
||||
<p class="subtitle is-5">{% trans "by" %} {% include 'snippets/authors.html' with book=book %}</p>
|
||||
{% endif %}
|
||||
{% if book|book_description %}
|
||||
<blockquote class="content">{{ book|book_description|to_markdown|safe|truncatewords_html:50 }}</blockquote>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% with book=book %}
|
||||
<div class="columns is-gapless">
|
||||
<div class="column is-5-tablet is-cover">
|
||||
<a
|
||||
class="align to-b to-l"
|
||||
href="{{ book.local_path }}"
|
||||
>{% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-w-auto-tablet' %}</a>
|
||||
|
||||
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="column mt-3-mobile ml-3-tablet">
|
||||
<h3 class="title is-5">
|
||||
<a href="{{ book.local_path }}">{{ book.title }}</a>
|
||||
</h3>
|
||||
|
||||
{% if book.authors %}
|
||||
<p class="subtitle is-5">
|
||||
{% trans "by" %}
|
||||
{% include 'snippets/authors.html' %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if book|book_description %}
|
||||
<blockquote class="content">
|
||||
{{ book|book_description|to_markdown|safe|truncatewords_html:50 }}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
|
|
@ -1,12 +1,24 @@
|
|||
|
||||
{% load bookwyrm_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if book %}
|
||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
|
||||
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
|
||||
{% with book=book %}
|
||||
<a href="{{ book.local_path }}">
|
||||
{% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-h-l-tablet is-w-auto align to-b to-l' %}
|
||||
</a>
|
||||
|
||||
<h3 class="title is-6"><a href="/book/{{ book.id }}">{{ book.title }}</a></h3>
|
||||
{% if book.authors %}
|
||||
<p class="subtitle is-6">{% trans "by" %} {% include 'snippets/authors.html' with book=book %}</p>
|
||||
{% endif %}
|
||||
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
|
||||
|
||||
<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 %}
|
||||
|
|
|
@ -76,8 +76,12 @@
|
|||
|
||||
<div class="block">
|
||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||
<a class="button" href="/author/{{ author.id }}">{% trans "Cancel" %}</a>
|
||||
<a class="button" href="{{ author.local_path }}">{% trans "Cancel" %}</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% include 'snippets/datepicker_js.html' %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -10,11 +10,11 @@
|
|||
{% trans "Direct Messages" %}
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if partner %}<p class="subtitle"><a href="/direct-messages"><span class="icon icon-arrow-left" aria-hidden="true"></span> {% trans "All messages" %}</a></p>{% endif %}
|
||||
{% if partner %}<p class="subtitle"><a href="{% url 'direct-messages' %}"><span class="icon icon-arrow-left" aria-hidden="true"></span> {% trans "All messages" %}</a></p>{% endif %}
|
||||
</header>
|
||||
|
||||
<div class="box">
|
||||
{% include 'snippets/create_status_form.html' with type="direct" uuid=1 mentions=partner %}
|
||||
{% include 'snippets/create_status_form.html' with type="direct" uuid=1 mention=partner %}
|
||||
</div>
|
||||
|
||||
<section class="block">
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
aria-label="{{ book.title }}"
|
||||
aria-selected="{% if active_book == book.id|stringformat:'d' %}true{% elif shelf_counter == 1 and forloop.first %}true{% else %}false{% endif %}"
|
||||
aria-controls="book-{{ book.id }}">
|
||||
{% include 'snippets/book_cover.html' with book=book size="medium" %}
|
||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
@ -105,4 +105,5 @@
|
|||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/vendor/tabs.js"></script>
|
||||
{% include 'snippets/datepicker_js.html' %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% load i18n %}
|
||||
<div class="column is-narrow is-clipped has-text-centered">
|
||||
{% include 'snippets/book_cover.html' with book=book %}
|
||||
<label class="label" for="id_shelve_{{ book.id }}">
|
||||
<div class="select is-small">
|
||||
<div class="column is-cover">
|
||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-l is-h-m-mobile' %}
|
||||
|
||||
<div class="select is-small mt-1 mb-3">
|
||||
<select name="{{ book.id }}" aria-label="{% blocktrans with book_title=book.title %}Have you read {{ book_title }}?{% endblocktrans %}">
|
||||
<option disabled selected value>Add to your books</option>
|
||||
{% for shelf in request.user.shelf_set.all %}
|
||||
|
|
|
@ -23,35 +23,48 @@
|
|||
|
||||
<form class="block" name="add-books" method="post" action="{% url 'get-started-books' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<h3 class="title is-5">{% trans "Suggested Books" %}</h3>
|
||||
<fieldset name="books" class="columns scroll-x is-mobile">
|
||||
{% if book_results %}
|
||||
<div class="column is-narrow content">
|
||||
<p class="help mb-0">Search results</p>
|
||||
<div class="columns is-mobile">
|
||||
{% for book in book_results %}
|
||||
{% include 'get_started/book_preview.html' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if popular_books %}
|
||||
<div class="column is-narrow content">
|
||||
<p class="help mb-0">
|
||||
{% blocktrans %}Popular on {{ site_name }}{% endblocktrans %}
|
||||
</p>
|
||||
<div class="columns is-mobile">
|
||||
{% for book in popular_books %}
|
||||
{% include 'get_started/book_preview.html' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not book_results and not popular_books %}
|
||||
<p><em>{% trans "No books found" %}</em></p>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
|
||||
<div class="block scroll-x">
|
||||
<fieldset name="books" class="columns is-mobile">
|
||||
{% if book_results %}
|
||||
<div class="column is-narrow">
|
||||
<p class="help mb-0">Search results</p>
|
||||
|
||||
<div class="columns is-mobile">
|
||||
{% for book in book_results %}
|
||||
{% include 'get_started/book_preview.html' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if popular_books %}
|
||||
<div class="column is-narrow">
|
||||
<p class="help mb-0">
|
||||
{% blocktrans %}Popular on {{ site_name }}{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<div class="columns is-mobile">
|
||||
{% for book in popular_books %}
|
||||
{% include 'get_started/book_preview.html' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not book_results and not popular_books %}
|
||||
<p><em>{% trans "No books found" %}</em></p>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button is-primary">{% trans "Save & continue" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{% include 'snippets/datepicker_js.html' %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'user/user_layout.html' %}
|
||||
{% extends 'user/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block header %}
|
||||
|
@ -45,21 +45,22 @@
|
|||
</section>
|
||||
|
||||
{% if goal.books %}
|
||||
<section class="content">
|
||||
<h2>
|
||||
<section>
|
||||
<h2 class="title is-4">
|
||||
{% if goal.user == request.user %}
|
||||
{% blocktrans %}Your {{ year }} Books{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans with username=goal.user.display_name %}{{ username }}'s {{ year }} Books{% endblocktrans %}
|
||||
{% endif %}
|
||||
</h2>
|
||||
<div class="columns is-multiline">
|
||||
|
||||
<div class="columns is-mobile is-multiline">
|
||||
{% for book in goal.books %}
|
||||
<div class="column is-one-fifth">
|
||||
<div class="is-clipped">
|
||||
<a href="{{ book.book.local_path }}">{% include 'snippets/book_cover.html' with book=book.book %}</a>
|
||||
<div class="column is-cover">
|
||||
<a href="{{ book.book.local_path }}">
|
||||
{% include 'snippets/book_cover.html' with book=book.book cover_class='is-h-xl is-h-l-mobile' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
{% endif %}
|
||||
<ul>
|
||||
{% for job in jobs %}
|
||||
<li><a href="/import/{{ job.id }}">{{ job.created_date | naturaltime }}</a></li>
|
||||
<li><a href="{% url 'import-status' job.id %}">{{ job.created_date | naturaltime }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -124,8 +124,8 @@
|
|||
<tr>
|
||||
<td>
|
||||
{% if item.book %}
|
||||
<a href="/book/{{ item.book.id }}">
|
||||
{% include 'snippets/book_cover.html' with book=item.book size='small' %}
|
||||
<a href="{{ item.book.local_path }}">
|
||||
{% include 'snippets/book_cover.html' with book=item.book cover_class='is-h-s' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
|
|
@ -94,12 +94,12 @@
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/import" class="navbar-item">
|
||||
<a href="{% url 'import' %}" class="navbar-item">
|
||||
{% trans 'Import Books' %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/preferences/profile" class="navbar-item">
|
||||
<a href="{% url 'prefs-profile' %}" class="navbar-item">
|
||||
{% trans 'Settings' %}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -122,21 +122,24 @@
|
|||
{% endif %}
|
||||
<li class="navbar-divider" role="presentation"></li>
|
||||
<li>
|
||||
<a href="/logout" class="navbar-item">
|
||||
<a href="{% url 'logout' %}" class="navbar-item">
|
||||
{% trans 'Log out' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
<a href="/notifications" class="tags has-addons">
|
||||
<a href="{% url 'notifications' %}" class="tags has-addons">
|
||||
<span class="tag is-medium">
|
||||
<span class="icon icon-bell" title="{% trans 'Notifications' %}">
|
||||
<span class="is-sr-only">{% trans "Notifications" %}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="{% if not request.user|notification_count %}is-hidden {% endif %}tag is-danger is-medium transition-x" data-poll-wrapper>
|
||||
<span data-poll="notifications">{{ request.user | notification_count }}</span>
|
||||
<span
|
||||
class="{% if not request.user.unread_notification_count %}is-hidden {% elif request.user.has_unread_mentions %}is-danger {% endif %}tag is-medium transition-x"
|
||||
data-poll-wrapper
|
||||
>
|
||||
<span data-poll="notifications">{{ request.user.unread_notification_count }}</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -155,7 +158,7 @@
|
|||
<div class="column">
|
||||
<label class="is-sr-only" for="id_password">{% trans "Username:" %}</label>
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password" placeholder="{% trans 'password' %}">
|
||||
<p class="help"><a href="/password-reset">{% trans "Forgot your password?" %}</a></p>
|
||||
<p class="help"><a href="{% url 'password-reset' %}">{% trans "Forgot your password?" %}</a></p>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<button class="button is-primary" type="submit">{% trans "Log in" %}</button>
|
||||
|
@ -190,25 +193,35 @@
|
|||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="column is-one-fifth">
|
||||
<p>
|
||||
<a href="/about">{% trans "About this server" %}</a>
|
||||
<a href="{% url 'about' %}">{% trans "About this server" %}</a>
|
||||
</p>
|
||||
{% if site.admin_email %}
|
||||
<p>
|
||||
<a href="mailto:{{ site.admin_email }}">{% trans "Contact site admin" %}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a href="https://docs.joinbookwyrm.com/">{% trans "Documentation" %}</a>
|
||||
</p>
|
||||
</div>
|
||||
{% 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>
|
||||
{% trans 'BookWyrm is open source software. You can contribute or report issues on <a href="https://github.com/mouse-reeve/bookwyrm">GitHub</a>.' %}
|
||||
</p>
|
||||
</div>
|
||||
{% if site.footer_item %}
|
||||
<div class="column">
|
||||
<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>{{ site.footer_item|safe }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="column">
|
||||
{% trans 'BookWyrm is open source software. You can contribute or report issues on <a href="https://github.com/mouse-reeve/bookwyrm">GitHub</a>.' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
@ -2,49 +2,71 @@
|
|||
{% load i18n %}
|
||||
{% block panel %}
|
||||
|
||||
<section class="content block">
|
||||
<h2>{% trans "Pending Books" %}</h2>
|
||||
<p><a href="{% url 'list' list.id %}">{% trans "Go to list" %}</a></p>
|
||||
<section class="block">
|
||||
<div class="columns is-mobile is-multiline is-align-items-baseline">
|
||||
<div class="column is-narrow">
|
||||
<h2 class="title is-4">{% trans "Pending Books" %}</h2>
|
||||
</div>
|
||||
|
||||
<p class="column is-narrow"><a href="{% url 'list' list.id %}">{% trans "Go to list" %}</a></p>
|
||||
</div>
|
||||
|
||||
{% if not pending.exists %}
|
||||
<p>{% trans "You're all set!" %}</p>
|
||||
<p>{% trans "You're all set!" %}</p>
|
||||
{% else %}
|
||||
<table class="table is-striped">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% trans "Book" %}</th>
|
||||
<th>{% trans "Suggested by" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
<dl>
|
||||
{% for item in pending %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=item.book size="small" %}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% include 'snippets/book_titleby.html' with book=item.book %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ item.user.local_path }}">{{ item.user.display_name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="field has-addons">
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% with book=item.book %}
|
||||
<div
|
||||
class="
|
||||
columns is-gapless
|
||||
is-vcentered is-justify-content-space-between
|
||||
mb-6
|
||||
"
|
||||
>
|
||||
<dt class="column mr-auto">
|
||||
<div class="columns is-mobile is-gapless is-vcentered">
|
||||
<a
|
||||
class="column is-cover"
|
||||
href="{{ book.local_path }}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{% include 'snippets/book_cover.html' with cover_class='is-w-xs-mobile is-w-s is-h-xs-mobile is-h-s' %}
|
||||
</a>
|
||||
|
||||
<div class="column ml-3">
|
||||
{% include 'snippets/book_titleby.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</dt>
|
||||
|
||||
<dd class="column is-4-tablet mx-3-tablet my-3-mobile">
|
||||
{% trans "Suggested by" %}
|
||||
|
||||
<a href="{{ item.user.local_path }}">
|
||||
{{ item.user.display_name }}
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
<dd class="column is-narrow field has-addons">
|
||||
<form class="control" method="POST" action="{% url 'list-curate' list.id %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item" value="{{ item.id }}">
|
||||
<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 %}
|
||||
</table>
|
||||
</dl>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -28,19 +28,38 @@
|
|||
{% else %}
|
||||
<ol start="{{ items.start_index }}" class="ordered-list">
|
||||
{% for item in items %}
|
||||
<li class="block pb-3">
|
||||
<li class="block mb-5">
|
||||
<div class="card">
|
||||
<div class="card-content columns p-0 pr-2 mb-0 is-mobile">
|
||||
<div class="column is-narrow pt-0 pb-0">
|
||||
<a href="{{ item.book.local_path }}">{% include 'snippets/book_cover.html' with book=item.book size="medium" %}</a>
|
||||
</div>
|
||||
<div class="column is-flex-direction-column is-align-items-self-start">
|
||||
<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 %}
|
||||
{% with book=item.book %}
|
||||
<div
|
||||
class="
|
||||
card-content p-0 mb-0
|
||||
columns is-mobile is-gapless
|
||||
"
|
||||
>
|
||||
<div class="column is-2-mobile is-cover align to-t">
|
||||
<a href="{{ item.book.local_path }}" aria-hidden="true">
|
||||
{% include 'snippets/book_cover.html' with cover_class='is-w-auto is-h-m-tablet' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="column ml-3">
|
||||
<p>
|
||||
{% include 'snippets/book_titleby.html' %}
|
||||
</p>
|
||||
<p>
|
||||
{% include 'snippets/stars.html' with rating=item.book|rating:request.user %}
|
||||
</p>
|
||||
<p>
|
||||
{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}
|
||||
</p>
|
||||
{% include 'snippets/shelve_button/shelve_button.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer is-stacked-mobile has-background-white-bis">
|
||||
{% endwith %}
|
||||
|
||||
<div class="card-footer is-stacked-mobile has-background-white-bis is-align-items-baseline">
|
||||
<div class="card-footer-item">
|
||||
<div>
|
||||
<p>{% blocktrans with username=item.user.display_name user_path=user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p>
|
||||
|
@ -78,7 +97,7 @@
|
|||
{% include "snippets/pagination.html" with page=items %}
|
||||
</section>
|
||||
|
||||
<section class="column is-one-quarter content">
|
||||
<section class="column is-one-quarter">
|
||||
<h2>{% trans "Sort List" %}</h2>
|
||||
<form name="sort" action="{% url 'list' list.id %}" method="GET" class="block">
|
||||
<div class="field">
|
||||
|
@ -124,24 +143,36 @@
|
|||
<p>{% trans "No books found" %}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% for book in suggested_books %}
|
||||
{% if book %}
|
||||
<div class="block columns is-mobile">
|
||||
<div class="column is-narrow">
|
||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<p>{% include 'snippets/book_titleby.html' with book=book %}</p>
|
||||
<form 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>
|
||||
|
||||
{% if suggested_books|length > 0 %}
|
||||
{% for book in suggested_books %}
|
||||
<div class="columns is-mobile is-gapless">
|
||||
<a
|
||||
class="column is-2-mobile is-3-tablet is-cover align to-c"
|
||||
href="{{ book.local_path }}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto is-h-s-mobile align to-t' %}
|
||||
</a>
|
||||
|
||||
<div class="column ml-3">
|
||||
<p>{% include 'snippets/book_titleby.html' with book=book %}</p>
|
||||
|
||||
<form
|
||||
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 %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
@ -8,11 +8,19 @@
|
|||
<a href="{{ list.local_path }}">{{ list.name }}</a> <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>
|
||||
</h4>
|
||||
</header>
|
||||
<div class="card-image is-flex is-clipped">
|
||||
{% for book in list.listitem_set.all|slice:5 %}
|
||||
<a href="{{ book.book.local_path }}">{% include 'snippets/book_cover.html' with book=book.book size="small" %}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% with list_books=list.listitem_set.all|slice:5 %}
|
||||
{% if list_books %}
|
||||
<div class="card-image columns is-mobile is-gapless is-clipped">
|
||||
{% for book in list_books %}
|
||||
<a class="column is-cover" href="{{ book.book.local_path }}">
|
||||
{% include 'snippets/book_cover.html' with book=book.book cover_class='is-h-s' aria='show' %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="card-content is-flex-grow-0">
|
||||
<div {% if list.description %}title="{{ list.description }}"{% endif %}>
|
||||
{% if list.description %}
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</div>
|
||||
|
||||
{% if lists %}
|
||||
<section class="block content">
|
||||
<section class="block">
|
||||
{% include 'lists/list_items.html' with lists=lists %}
|
||||
</section>
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<button class="button is-primary" type="submit">{% trans "Log in" %}</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<small><a href="/password-reset">{% trans "Forgot your password?" %}</a></small>
|
||||
<small><a href="{% url 'password-reset' %}">{% trans "Forgot your password?" %}</a></small>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -56,7 +56,7 @@
|
|||
{% include 'snippets/about.html' %}
|
||||
|
||||
<p class="block">
|
||||
<a href="/about/">{% trans "More about this site" %}</a>
|
||||
<a href="{% url 'about' %}">{% trans "More about this site" %}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -107,7 +107,8 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
{% elif notification.related_import %}
|
||||
{% blocktrans with related_id=notification.related_import.id %}Your <a href="/import/{{ related_id }}">import</a> completed.{% endblocktrans %}
|
||||
{% url 'import-status' notification.related_import.id as url %}
|
||||
{% blocktrans %}Your <a href="{{ url }}">import</a> completed.{% endblocktrans %}
|
||||
{% elif notification.related_report %}
|
||||
{% url 'settings-report' notification.related_report.id as path %}
|
||||
{% blocktrans with related_id=path %}A new <a href="{{ path }}">report</a> needs moderation.{% endblocktrans %}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<form name="edit-profile" action="/change-password/" method="post" enctype="multipart/form-data">
|
||||
<form name="edit-profile" action="{% url 'prefs-password' %}" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="block">
|
||||
<label class="label" for="id_password">{% trans "New password:" %}</label>
|
||||
|
|
|
@ -11,16 +11,19 @@
|
|||
<h2 class="menu-label">{% trans "Account" %}</h2>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<a href="/preferences/profile"{% if '/preferences/profile' in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Profile" %}</a>
|
||||
{% url 'prefs-profile' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Profile" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/preferences/password"{% if '/preferences/password' in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Change Password" %}</a>
|
||||
{% url 'prefs-password' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Change Password" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 class="menu-label">{% trans "Relationships" %}</h2>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<a href="/preferences/block"{% if '/preferences/block' in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Blocked Users" %}</a>
|
||||
{% url 'prefs-block' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Blocked Users" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
78
bookwyrm/templates/search/book.html
Normal file
78
bookwyrm/templates/search/book.html
Normal 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">
|
||||
{% 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 "Show" as button_text %}
|
||||
{% include 'snippets/toggle/open_button.html' with text=button_text small=True controls_text="more-results-panel" controls_uid=result_set.connector.identifier class="is-small" icon="arrow-down" 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>
|
||||
{% trans "Close" as button_text %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with label=button_text class="delete" nonbutton=True controls_text="more-results-panel" controls_uid=result_set.connector.identifier pressed=forloop.first %}
|
||||
</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 %}
|
70
bookwyrm/templates/search/layout.html
Normal file
70
bookwyrm/templates/search/layout.html
Normal 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 %}
|
7
bookwyrm/templates/search/list.html
Normal file
7
bookwyrm/templates/search/list.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
{% extends 'search/layout.html' %}
|
||||
|
||||
{% block panel %}
|
||||
|
||||
{% include 'lists/list_items.html' with lists=results %}
|
||||
|
||||
{% endblock %}
|
14
bookwyrm/templates/search/user.html
Normal file
14
bookwyrm/templates/search/user.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends 'search/layout.html' %}
|
||||
{% load bookwyrm_tags %}
|
||||
|
||||
{% 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 %}
|
|
@ -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 %}
|
|
@ -37,16 +37,16 @@
|
|||
|
||||
<section class="block" id="images">
|
||||
<h2 class="title is-4">{% trans "Images" %}</h2>
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<label class="label" for="id_logo">{% trans "Logo:" %}</label>
|
||||
{{ site_form.logo }}
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="column">
|
||||
<label class="label" for="id_logo_small">{% trans "Logo small:" %}</label>
|
||||
{{ site_form.logo_small }}
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="column">
|
||||
<label class="label" for="id_favicon">{% trans "Favicon:" %}</label>
|
||||
{{ site_form.favicon }}
|
||||
</div>
|
||||
|
@ -69,6 +69,10 @@
|
|||
<label class="label" for="id_admin_email">{% trans "Admin email:" %}</label>
|
||||
{{ site_form.admin_email }}
|
||||
</div>
|
||||
<div class="control">
|
||||
<label class="label" for="id_footer_item">{% trans "Additional info:" %}</label>
|
||||
{{ site_form.footer_item }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr aria-hidden="true">
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% endcomment %}
|
||||
{% for author in book.authors.all %}
|
||||
<a
|
||||
href="/author/{{ author.id }}"
|
||||
href="{{ author.local_path }}"
|
||||
class="author"
|
||||
itemprop="author"
|
||||
itemscope
|
||||
|
|
|
@ -3,27 +3,40 @@
|
|||
{% load bookwyrm_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="cover-container is-{{ size }}">
|
||||
{% if book.cover %}
|
||||
<img
|
||||
class="book-cover"
|
||||
src="/images/{{ book.cover }}"
|
||||
alt="{{ book.alt_text }}"
|
||||
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" %}"
|
||||
>
|
||||
<figure
|
||||
class="
|
||||
cover-container
|
||||
{{ cover_class }}
|
||||
|
||||
<div>
|
||||
<p>{{ book.alt_text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if not book.cover %}
|
||||
no-cover
|
||||
{% endif %}
|
||||
"
|
||||
|
||||
{% if book.alt_text %}
|
||||
title="{{ book.alt_text }}"
|
||||
{% 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 %}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<div class="columns is-multiline">
|
||||
<div class="columns is-mobile 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>
|
||||
<div class="box is-flex is-flex-direction-column is-align-items-center">
|
||||
<div class="mb-3">
|
||||
<a href="{{ book.local_path }}">
|
||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-l-mobile is-h-l-mobile is-w-l-tablet is-h-xl-tablet' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book switch_mode=True %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -36,13 +36,23 @@
|
|||
|
||||
<div class="control">
|
||||
{% if type == 'quotation' %}
|
||||
<textarea name="quote" class="textarea" id="id_quote_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required>{{ draft.quote|default:'' }}</textarea>
|
||||
{% elif type == 'reply' %}
|
||||
{% include 'snippets/content_warning_field.html' with parent_status=status %}
|
||||
<textarea name="content" class="textarea" id="id_content_{{ type }}-{{ book.id }}{{reply_parent.id}}" placeholder="{{ placeholder }}" aria-label="{% trans 'Reply' %}" required>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mentions %}@{{ mentions|username }} {% endif %}{{ draft.content|default:'' }}</textarea>
|
||||
<textarea
|
||||
name="quote"
|
||||
class="textarea"
|
||||
id="id_quote_{{ book.id }}_{{ type }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
required
|
||||
>{{ draft.quote|default:'' }}</textarea>
|
||||
{% else %}
|
||||
{% include 'snippets/content_warning_field.html' with parent_status=status %}
|
||||
<textarea name="content" class="textarea" id="id_content_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required>{{ draft.content|default:'' }}</textarea>
|
||||
<textarea
|
||||
name="content"
|
||||
class="textarea"
|
||||
id="id_content_{{ type }}-{{ book.id }}{{reply_parent.id}}"
|
||||
placeholder="{{ placeholder }}"
|
||||
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans "Content" %}{% endif %}"
|
||||
required
|
||||
>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mention %}@{{ mention|username }} {% endif %}{{ draft.content|default:'' }}</textarea>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
3
bookwyrm/templates/snippets/datepicker_js.html
Normal file
3
bookwyrm/templates/snippets/datepicker_js.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<script type="module" src="https://cdn.jsdelivr.net/npm/@duetds/date-picker@1.3.0/dist/duet/duet.esm.js"></script>
|
||||
<script nomodule src="https://cdn.jsdelivr.net/npm/@duetds/date-picker@1.3.0/dist/duet/duet.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@duetds/date-picker@1.3.0/dist/duet/themes/default.css" />
|
|
@ -1,5 +1,5 @@
|
|||
{% load i18n %}
|
||||
<nav class="pagination" aria-label="pagination">
|
||||
<nav class="pagination is-centered" aria-label="pagination">
|
||||
<a
|
||||
class="pagination-previous {% if not page.has_previous %}is-disabled{% endif %}"
|
||||
{% if page.has_previous %}
|
||||
|
@ -23,4 +23,18 @@
|
|||
{% trans "Next" %}
|
||||
<span class="icon icon-arrow-right" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
||||
{% if page.has_other_pages and page_range %}
|
||||
<ul class="pagination-list">
|
||||
{% for num in page_range %}
|
||||
{% if num == page.number %}
|
||||
<li><a class="pagination-link is-current" aria-label="Page {{ num }}" aria-current="page">{{ num }}</a></li>
|
||||
{% elif num == '…' %}
|
||||
<li><span class="pagination-ellipsis">…</span></li>
|
||||
{% else %}
|
||||
<li><a class="pagination-link" aria-label="Goto page {{ num }}" href="{{ path }}?{% for k, v in request.GET.items %}{% if k != 'page' %}{{ k }}={{ v }}&{% endif %}{% endfor %}page={{ num }}{{ anchor }}">{{ num }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load tz %}
|
||||
<div class="content block">
|
||||
<div class="content box is-shadowless has-background-white-bis">
|
||||
<div id="hide-edit-readthrough-{{ readthrough.id }}">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
|
@ -48,7 +48,9 @@
|
|||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if readthrough.start_date %}
|
||||
<li>{{ readthrough.start_date | localtime | naturalday }}: {% trans "started" %}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
|
|
|
@ -5,7 +5,11 @@
|
|||
<div class="field">
|
||||
<label class="label">
|
||||
{% trans "Started reading" %}
|
||||
<input type="date" name="start_date" class="input" id="id_start_date-{{ readthrough.id }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
|
||||
<duet-date-picker
|
||||
identifier="id_start_date-{{ readthrough.id }}"
|
||||
name="start_date"
|
||||
value="{{ readthrough.start_date | date:'Y-m-d' }}">
|
||||
</duet-date-picker>
|
||||
</label>
|
||||
</div>
|
||||
{# Only show progress for editing existing readthroughs #}
|
||||
|
@ -28,6 +32,10 @@
|
|||
<div class="field">
|
||||
<label class="label">
|
||||
{% trans "Finished reading" %}
|
||||
<input type="date" name="finish_date" class="input" id="id_finish_date-{{ readthrough.id }}" value="{{ readthrough.finish_date | date:"Y-m-d" }}">
|
||||
<duet-date-picker
|
||||
identifier="id_finish_date-{{ readthrough.id }}"
|
||||
name="finish_date"
|
||||
value="{{ readthrough.finish_date | date:'Y-m-d' }}">
|
||||
</duet-date-picker>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -1,34 +1,41 @@
|
|||
{% load i18n %}
|
||||
<div class="columns is-mobile">
|
||||
<div class="cover-container is-small column is-2">
|
||||
{% if result.cover %}
|
||||
<img src="{{ result.cover }}" class="book-cover" aria-hidden="true">
|
||||
{% else %}
|
||||
<div class="no-cover book-cover">
|
||||
<img class="book-cover" src="/static/images/no_cover.jpg" aria-hidden="true">
|
||||
<div>
|
||||
<p>{% trans "No cover" %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="columns is-mobile is-gapless">
|
||||
<div class="column is-cover">
|
||||
{% include 'snippets/book_cover.html' with book=result cover_class='is-w-xs is-h-xs' img_path=false %}
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<div class="column is-10 ml-3">
|
||||
<p>
|
||||
<strong>
|
||||
<a href="{{ result.key }}"{% if remote_result %} rel=”noopener” target="_blank"{% endif %}>{{ result.title }}</a>
|
||||
<a
|
||||
href="{{ result.view_link|default:result.key }}"
|
||||
{% if remote_result %}
|
||||
rel=”noopener”
|
||||
target="_blank"
|
||||
{% endif %}
|
||||
>{{ result.title }}</a>
|
||||
</strong>
|
||||
</p>
|
||||
<p>
|
||||
{% if result.author %}
|
||||
{% blocktrans with author=result.author %}by {{ author }}{% endblocktrans %}{% endif %}{% if result.year %} ({{ result.year }})
|
||||
{{ result.author }}
|
||||
{% endif %}
|
||||
|
||||
{% if result.year %}
|
||||
({{ result.year }})
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if remote_result %}
|
||||
<form action="/resolve-book" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="remote_id" value="{{ result.key }}">
|
||||
<button type="submit" class="button is-small is-link">{% trans "Import book" %}</button>
|
||||
</form>
|
||||
<form class="mt-1" action="/resolve-book" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<input type="hidden" name="remote_id" value="{{ result.key }}">
|
||||
|
||||
<button type="submit" class="button is-small is-link">
|
||||
{% trans "Import book" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -17,13 +17,21 @@
|
|||
<div class="field">
|
||||
<label class="label">
|
||||
{% trans "Started reading" %}
|
||||
<input type="date" name="start_date" class="input" id="finish_id_start_date-{{ uuid }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
|
||||
<duet-date-picker
|
||||
identifier="id_start_date-{{ uuid }}"
|
||||
name="start_date"
|
||||
value="{{ readthrough.start_date | date:'Y-m-d' }}"
|
||||
></duet-date-picker>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
{% trans "Finished reading" %}
|
||||
<input type="date" name="finish_date" class="input" id="id_finish_date-{{ uuid }}" value="{% now "Y-m-d" %}">
|
||||
<duet-date-picker
|
||||
identifier="id_finish_date-{{ uuid }}"
|
||||
name="finish_date"
|
||||
value="{{ readthrough.finish_date | date:'Y-m-d' }}"
|
||||
></duet-date-picker>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -15,7 +15,11 @@
|
|||
<div class="field">
|
||||
<label class="label">
|
||||
{% trans "Started reading" %}
|
||||
<input type="date" name="start_date" class="input" id="start_id_start_date-{{ uuid }}" value="{% now "Y-m-d" %}">
|
||||
<duet-date-picker
|
||||
identifier="start_id_start_date-{{ uuid }}"
|
||||
name="start_date"
|
||||
value="{% now "Y-m-d" %}"
|
||||
></duet-date-picker>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -10,27 +10,30 @@
|
|||
{% endif %}
|
||||
>
|
||||
|
||||
<div class="columns">
|
||||
<div class="columns is-gapless">
|
||||
{% if not hide_book %}
|
||||
{% with book=status.book|default:status.mention_books.first %}
|
||||
{% if book %}
|
||||
<div class="column is-narrow">
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-narrow">
|
||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
|
||||
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
|
||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||
</div>
|
||||
<div class="column is-hidden-tablet">
|
||||
<p>{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:15 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% with book=status.book|default:status.mention_books.first %}
|
||||
{% if book %}
|
||||
<div class="column is-cover">
|
||||
<div class="columns is-mobile is-gapless">
|
||||
<div class="column is-cover">
|
||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book cover_class='is-w-s-mobile is-h-l-tablet' %}</a>
|
||||
|
||||
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
|
||||
|
||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||
</div>
|
||||
|
||||
<div class="column ml-3-mobile is-hidden-tablet">
|
||||
<p>{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:15 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
<article class="column">
|
||||
<article class="column ml-3-tablet my-3-mobile">
|
||||
{% if status_type == 'Review' %}
|
||||
<header class="mb-2">
|
||||
<h3
|
||||
|
|
|
@ -4,20 +4,25 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% if not hide_book %}
|
||||
{% with book=status.book|default:status.mention_books.first %}
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-narrow">
|
||||
<div>
|
||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a>
|
||||
{% with book=status.book|default:status.mention_books.first %}
|
||||
<div class="columns is-mobile is-gapless">
|
||||
<a class="column is-cover is-narrow" href="{{ book.local_path }}">
|
||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-xs is-h-s-tablet' %}
|
||||
</a>
|
||||
|
||||
<div class="column ml-3">
|
||||
<h3 class="title is-6 mb-1">
|
||||
{% include 'snippets/book_titleby.html' with book=book %}
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}
|
||||
</p>
|
||||
|
||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h3 class="title is-6 mb-1">{% include 'snippets/book_titleby.html' with book=book %}</h3>
|
||||
<p>{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}</p>
|
||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% endspaceless %}
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
|
||||
{% else %}
|
||||
<div class="card-footer-item">
|
||||
<a href="/login">
|
||||
<a href="{% url 'login' %}">
|
||||
<span class="icon icon-comment is-small" title="{% trans 'Reply' %}">
|
||||
<span class="is-sr-only">{% trans "Reply" %}</span>
|
||||
</span>
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
{% if status.book %}
|
||||
{% if status.status_type == 'GeneratedNote' or status.status_type == 'Rating' %}
|
||||
<a href="/book/{{ status.book.id }}">{{ status.book|title }}</a>{% if status.status_type == 'Rating' %}: {% include 'snippets/stars.html' with rating=status.rating %}
|
||||
<a href="{{ status.book.local_path }}">{{ status.book|title }}</a>{% if status.status_type == 'Rating' %}: {% include 'snippets/stars.html' with rating=status.rating %}
|
||||
<span
|
||||
itemprop="reviewRating"
|
||||
itemscope
|
||||
|
@ -58,7 +58,7 @@
|
|||
|
||||
{% if status.book %}
|
||||
{% if status.status_type == 'GeneratedNote' or status.status_type == 'Rating' %}
|
||||
<a href="/book/{{ status.book.id }}">{{ status.book|title }}</a>{% if status.status_type == 'Rating' %}:
|
||||
<a href="{{ status.book.local_path }}">{{ status.book|title }}</a>{% if status.status_type == 'Rating' %}:
|
||||
<span
|
||||
itemprop="reviewRating"
|
||||
itemscope
|
||||
|
@ -76,7 +76,7 @@
|
|||
{% include 'snippets/book_titleby.html' with book=status.book %}
|
||||
{% endif %}
|
||||
{% elif status.mention_books %}
|
||||
<a href="/book/{{ status.mention_books.first.id }}">
|
||||
<a href="{{ status.mention_books.first.local_path }}">
|
||||
{{ status.mention_books.first.title }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -86,7 +86,7 @@
|
|||
{% include 'snippets/book_titleby.html' with book=status.book %}
|
||||
{% endif %}
|
||||
{% elif status.mention_books %}
|
||||
<a href="/book/{{ status.mention_books.first.id }}">{{ status.mention_books.first|title }}</a>
|
||||
<a href="{{ status.mention_books.first.local_path }}">{{ status.mention_books.first|title }}</a>
|
||||
{% endif %}
|
||||
|
||||
</h3>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
{% block dropdown-list %}
|
||||
<li role="menuitem">
|
||||
<a href="/direct-messages/{{ user|username }}" class="button is-fullwidth is-small">{% trans "Send direct message" %}</a>
|
||||
<a href="{% url 'direct-messages-user' user|username %}" class="button is-fullwidth is-small">{% trans "Send direct message" %}</a>
|
||||
</li>
|
||||
<li role="menuitem">
|
||||
{% include 'snippets/report_button.html' with user=user class="is-fullwidth" %}
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
{% extends 'user/user_layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
|
||||
{% block header %}
|
||||
<h1 class="title">
|
||||
{% trans "User Profile" %}
|
||||
</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<div class="block">
|
||||
<h2 class="title">{% trans "Followers" %}</h2>
|
||||
{% for follower in followers %}
|
||||
<div class="block columns">
|
||||
<div class="column">
|
||||
<a href="{{ follower.local_path }}">
|
||||
{% include 'snippets/avatar.html' with user=follower %}
|
||||
{{ follower.display_name }}
|
||||
</a>
|
||||
({{ follower.username }})
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
{% include 'snippets/follow_button.html' with user=follower %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not followers.count %}
|
||||
<div>{% blocktrans with username=user.display_name %}{{ username }} has no followers{% endblocktrans %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% include 'snippets/pagination.html' with page=followers path=request.path %}
|
||||
{% endblock %}
|
|
@ -1,34 +0,0 @@
|
|||
{% extends 'user/user_layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
|
||||
{% block header %}
|
||||
<h1 class="title">
|
||||
{% trans "User Profile" %}
|
||||
</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<div class="block">
|
||||
<h2 class="title">{% trans "Following" %}</h2>
|
||||
{% for follower in user.following.all %}
|
||||
<div class="block columns">
|
||||
<div class="column">
|
||||
<a href="{{ follower.local_path }}">
|
||||
{% include 'snippets/avatar.html' with user=follower %}
|
||||
{{ follower.display_name }}
|
||||
</a>
|
||||
({{ follower.username }})
|
||||
</div>
|
||||
<div class="column">
|
||||
{% include 'snippets/follow_button.html' with user=follower %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not following.count %}
|
||||
<div>{% blocktrans with username=user|username %}{{ username }} isn't following any users{% endblocktrans %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% include 'snippets/pagination.html' with page=following path=request.path %}
|
||||
{% endblock %}
|
|
@ -7,7 +7,11 @@
|
|||
|
||||
{% block content %}
|
||||
<header class="block">
|
||||
{% block header %}{% endblock %}
|
||||
{% block header %}
|
||||
<h1 class="title">
|
||||
{% trans "User Profile" %}
|
||||
</h1>
|
||||
{% endblock %}
|
||||
</header>
|
||||
|
||||
{# user bio #}
|
||||
|
@ -41,8 +45,9 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% block tabs %}
|
||||
{% with user|username as username %}
|
||||
{% if 'user/'|add:username|add:'/books' not in request.path and 'user/'|add:username|add:'/shelf' not in request.path %}
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
{% url 'user-feed' user|username as url %}
|
||||
|
@ -70,9 +75,14 @@
|
|||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{% include 'snippets/datepicker_js.html' %}
|
||||
{% endblock %}
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'user/user_layout.html' %}
|
||||
{% extends 'user/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block header %}
|
||||
|
@ -23,7 +23,7 @@
|
|||
|
||||
|
||||
{% block panel %}
|
||||
<section class="block content">
|
||||
<section class="block">
|
||||
<form name="create-list" method="post" action="{% url 'lists' %}" class="box is-hidden" id="create-list">
|
||||
<header class="columns">
|
||||
<h3 class="title column">{% trans "Create list" %}</h3>
|
||||
|
|
14
bookwyrm/templates/user/relationships/followers.html
Normal file
14
bookwyrm/templates/user/relationships/followers.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends 'user/relationships/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block header %}
|
||||
<h1 class="title">
|
||||
{% trans "Followers" %}
|
||||
</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block nullstate %}
|
||||
<div>
|
||||
{% blocktrans with username=user.display_name %}{{ username }} has no followers{% endblocktrans %}
|
||||
</div>
|
||||
{% endblock %}
|
14
bookwyrm/templates/user/relationships/following.html
Normal file
14
bookwyrm/templates/user/relationships/following.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends 'user/relationships/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block header %}
|
||||
<h1 class="title">
|
||||
{% trans "Following" %}
|
||||
</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block nullstate %}
|
||||
<div>
|
||||
{% blocktrans with username=user.display_name %}{{ username }} isn't following any users{% endblocktrans %}
|
||||
</div>
|
||||
{% endblock %}
|
46
bookwyrm/templates/user/relationships/layout.html
Normal file
46
bookwyrm/templates/user/relationships/layout.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
{% extends 'user/layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
|
||||
{% block tabs %}
|
||||
{% with user|username as username %}
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
{% url 'user-followers' user|username as url %}
|
||||
<li{% if url == request.path or url == request.path|add:'/' %} class="is-active"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Followers" %}</a>
|
||||
</li>
|
||||
{% url 'user-following' user|username as url %}
|
||||
<li{% if url == request.path or url == request.path|add:'/' %} class="is-active"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Following" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<div class="block">
|
||||
{% for follow in follow_list %}
|
||||
<div class="block columns">
|
||||
<div class="column">
|
||||
<a href="{{ follow.local_path }}">
|
||||
{% include 'snippets/avatar.html' with user=follow %}
|
||||
{{ follow.display_name }}
|
||||
</a>
|
||||
({{ follow.username }})
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
{% include 'snippets/follow_button.html' with user=follow %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if not follow_list %}
|
||||
{% block nullstate %}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% include 'snippets/pagination.html' with page=follow_list path=request.path %}
|
||||
{% endblock %}
|
|
@ -1,21 +1,21 @@
|
|||
{% extends 'user/user_layout.html' %}
|
||||
{% extends 'user/layout.html' %}
|
||||
{% load bookwyrm_tags %}
|
||||
{% load humanize %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
{% include 'user/books_header.html' %}
|
||||
{% include 'user/shelf/books_header.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<header class="columns">
|
||||
<h1 class="title">
|
||||
{% include 'user/books_header.html' %}
|
||||
{% include 'user/shelf/books_header.html' %}
|
||||
</h1>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
{% block tabs %}
|
||||
<div class="block columns">
|
||||
<div class="column">
|
||||
<div class="tabs">
|
||||
|
@ -39,9 +39,11 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<div class="block">
|
||||
{% include 'user/create_shelf_form.html' with controls_text='create-shelf-form' %}
|
||||
{% include 'user/shelf/create_shelf_form.html' with controls_text='create-shelf-form' %}
|
||||
</div>
|
||||
|
||||
<div class="block columns is-mobile">
|
||||
|
@ -62,7 +64,7 @@
|
|||
</div>
|
||||
|
||||
<div class="block">
|
||||
{% include 'user/edit_shelf_form.html' with controls_text="edit-shelf-form" %}
|
||||
{% include 'user/shelf/edit_shelf_form.html' with controls_text="edit-shelf-form" %}
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
|
@ -88,7 +90,7 @@
|
|||
{% spaceless %}
|
||||
<tr class="book-preview">
|
||||
<td class="book-preview-top-row">
|
||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a>
|
||||
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book cover_class='is-w-s-tablet is-h-s' %}</a>
|
||||
</td>
|
||||
<td data-title="{% trans "Title" %}">
|
||||
<a href="{{ book.local_path }}">{{ book.title }}</a>
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'user/user_layout.html' %}
|
||||
{% extends 'user/layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load bookwyrm_tags %}
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
|||
</div>
|
||||
{% if is_self %}
|
||||
<div class="column is-narrow">
|
||||
<a href="/preferences/profile">
|
||||
<a href="{% url 'prefs-profile' %}">
|
||||
<span class="icon icon-pencil" title="Edit profile">
|
||||
<span class="is-sr-only">{% trans "Edit profile" %}</span>
|
||||
</span>
|
||||
|
@ -25,7 +25,7 @@
|
|||
{% if user.bookwyrm_user %}
|
||||
<div class="block">
|
||||
<h2 class="title">
|
||||
{% include 'user/books_header.html' %}
|
||||
{% include 'user/shelf/books_header.html' %}
|
||||
</h2>
|
||||
<div class="columns">
|
||||
{% for shelf in shelves %}
|
||||
|
@ -36,7 +36,7 @@
|
|||
{% for book in shelf.books %}
|
||||
<div class="control">
|
||||
<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-h-m is-h-s-mobile' %}
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
@ -53,11 +53,6 @@
|
|||
<h2 class="title">{% now 'Y' %} Reading Goal</h2>
|
||||
{% include 'snippets/goal_progress.html' with goal=goal %}
|
||||
</div>
|
||||
{% elif user == request.user %}
|
||||
<div class="block">
|
||||
{% now 'Y' as year %}
|
||||
<h2 class="title is-4"><a href="{{ user.local_path }}/goal/{{ year }}">{% blocktrans %}Set a reading goal for {{ year }}{% endblocktrans %}</a></h2>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load bookwyrm_tags %}
|
||||
|
||||
<div class="media block">
|
||||
<div class="media-left">
|
||||
|
@ -12,8 +13,19 @@
|
|||
<p><a href="{{ user.remote_id }}">{{ user.username }}</a></p>
|
||||
<p>{% blocktrans with date=user.created_date|naturaltime %}Joined {{ date }}{% endblocktrans %}</p>
|
||||
<p>
|
||||
<a href="{{ user.local_path }}/followers">{% blocktrans count counter=user.followers.count %}{{ counter }} follower{% plural %}{{ counter }} followers{% endblocktrans %}</a>,
|
||||
<a href="{{ user.local_path }}/following">{% blocktrans with counter=user.following.count %}{{ counter }} following{% endblocktrans %}</a>
|
||||
{% if is_self %}
|
||||
|
||||
<a href="{% url 'user-followers' user|username %}">{% blocktrans count counter=user.followers.count %}{{ counter }} follower{% plural %}{{ counter }} followers{% endblocktrans %}</a>,
|
||||
<a href="{% url 'user-following' user|username %}">{% blocktrans with counter=user.following.count %}{{ counter }} following{% endblocktrans %}</a>
|
||||
|
||||
{% elif request.user.is_authenticated %}
|
||||
|
||||
{% mutuals_count user as mutuals %}
|
||||
<a href="{% url 'user-followers' user|username %}">
|
||||
{% blocktrans with mutuals_display=mutuals|intcomma count counter=mutuals %}{{ mutuals_display }} follower you follow{% plural %}{{ mutuals_display }} followers you follow{% endblocktrans %}
|
||||
</a>
|
||||
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -235,3 +235,12 @@ def get_lang():
|
|||
"""get current language, strip to the first two letters"""
|
||||
language = utils.translation.get_language()
|
||||
return language[0 : language.find("-")]
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def mutuals_count(context, user):
|
||||
"""how many users that you follow, follow them"""
|
||||
viewer = context["request"].user
|
||||
if not viewer.is_authenticated:
|
||||
return None
|
||||
return user.followers.filter(id__in=viewer.following.all()).count()
|
||||
|
|
|
@ -17,8 +17,6 @@ class ConnectorManager(TestCase):
|
|||
self.edition = models.Edition.objects.create(
|
||||
title="Example Edition", parent_work=self.work, isbn_10="0000000000"
|
||||
)
|
||||
self.work.default_edition = self.edition
|
||||
self.work.save()
|
||||
|
||||
self.connector = models.Connector.objects.create(
|
||||
identifier="test_connector",
|
||||
|
|
173
bookwyrm/tests/connectors/test_inventaire_connector.py
Normal file
173
bookwyrm/tests/connectors/test_inventaire_connector.py
Normal file
|
@ -0,0 +1,173 @@
|
|||
""" testing book data connectors """
|
||||
import json
|
||||
import pathlib
|
||||
from django.test import TestCase
|
||||
import responses
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.connectors.inventaire import Connector, get_language_code
|
||||
|
||||
|
||||
class Inventaire(TestCase):
|
||||
"""test loading data from inventaire.io"""
|
||||
|
||||
def setUp(self):
|
||||
"""creates the connector we'll use"""
|
||||
models.Connector.objects.create(
|
||||
identifier="inventaire.io",
|
||||
name="Inventaire",
|
||||
connector_file="inventaire",
|
||||
base_url="https://inventaire.io",
|
||||
books_url="https://inventaire.io",
|
||||
covers_url="https://covers.inventaire.io",
|
||||
search_url="https://inventaire.io/search?q=",
|
||||
isbn_search_url="https://inventaire.io/isbn",
|
||||
)
|
||||
self.connector = Connector("inventaire.io")
|
||||
|
||||
@responses.activate
|
||||
def test_get_book_data(self):
|
||||
"""flattens the default structure to make it easier to parse"""
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://test.url/ok",
|
||||
json={
|
||||
"entities": {
|
||||
"isbn:9780375757853": {
|
||||
"claims": {
|
||||
"wdt:P31": ["wd:Q3331189"],
|
||||
},
|
||||
"uri": "isbn:9780375757853",
|
||||
}
|
||||
},
|
||||
"redirects": {},
|
||||
},
|
||||
)
|
||||
|
||||
result = self.connector.get_book_data("https://test.url/ok")
|
||||
self.assertEqual(result["wdt:P31"], ["wd:Q3331189"])
|
||||
self.assertEqual(result["uri"], "isbn:9780375757853")
|
||||
|
||||
def test_format_search_result(self):
|
||||
"""json to search result objs"""
|
||||
search_file = pathlib.Path(__file__).parent.joinpath(
|
||||
"../data/inventaire_search.json"
|
||||
)
|
||||
search_results = json.loads(search_file.read_bytes())
|
||||
|
||||
results = self.connector.parse_search_data(search_results)
|
||||
formatted = self.connector.format_search_result(results[0])
|
||||
|
||||
self.assertEqual(formatted.title, "The Stories of Vladimir Nabokov")
|
||||
self.assertEqual(
|
||||
formatted.key, "https://inventaire.io?action=by-uris&uris=wd:Q7766679"
|
||||
)
|
||||
self.assertEqual(
|
||||
formatted.cover,
|
||||
"https://covers.inventaire.io/img/entities/ddb32e115a28dcc0465023869ba19f6868ec4042",
|
||||
)
|
||||
|
||||
def test_get_cover_url(self):
|
||||
"""figure out where the cover image is"""
|
||||
cover_blob = {"url": "/img/entities/d46a8"}
|
||||
result = self.connector.get_cover_url(cover_blob)
|
||||
self.assertEqual(result, "https://covers.inventaire.io/img/entities/d46a8")
|
||||
|
||||
cover_blob = {
|
||||
"url": "https://commons.wikimedia.org/wiki/Special:FilePath/The%20Moonstone%201st%20ed.jpg?width=1000",
|
||||
"file": "The Moonstone 1st ed.jpg",
|
||||
"credits": {
|
||||
"text": "Wikimedia Commons",
|
||||
"url": "https://commons.wikimedia.org/wiki/File:The Moonstone 1st ed.jpg",
|
||||
},
|
||||
}
|
||||
|
||||
result = self.connector.get_cover_url(cover_blob)
|
||||
self.assertEqual(
|
||||
result,
|
||||
"https://commons.wikimedia.org/wiki/Special:FilePath/The%20Moonstone%201st%20ed.jpg?width=1000",
|
||||
)
|
||||
|
||||
@responses.activate
|
||||
def test_resolve_keys(self):
|
||||
"""makes an http request"""
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://inventaire.io?action=by-uris&uris=wd:Q465821",
|
||||
json={
|
||||
"entities": {
|
||||
"wd:Q465821": {
|
||||
"type": "genre",
|
||||
"labels": {
|
||||
"nl": "briefroman",
|
||||
"en": "epistolary novel",
|
||||
"de-ch": "Briefroman",
|
||||
"en-ca": "Epistolary novel",
|
||||
"nb": "brev- og dagbokroman",
|
||||
},
|
||||
"descriptions": {
|
||||
"en": "novel written as a series of documents",
|
||||
"es": "novela escrita como una serie de documentos",
|
||||
"eo": "romano en la formo de serio de leteroj",
|
||||
},
|
||||
},
|
||||
"redirects": {},
|
||||
}
|
||||
},
|
||||
)
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://inventaire.io?action=by-uris&uris=wd:Q208505",
|
||||
json={
|
||||
"entities": {
|
||||
"wd:Q208505": {
|
||||
"type": "genre",
|
||||
"labels": {
|
||||
"en": "crime novel",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
keys = [
|
||||
"wd:Q465821",
|
||||
"wd:Q208505",
|
||||
]
|
||||
result = self.connector.resolve_keys(keys)
|
||||
self.assertEqual(result, ["epistolary novel", "crime novel"])
|
||||
|
||||
def test_isbn_search(self):
|
||||
"""another search type"""
|
||||
search_file = pathlib.Path(__file__).parent.joinpath(
|
||||
"../data/inventaire_isbn_search.json"
|
||||
)
|
||||
search_results = json.loads(search_file.read_bytes())
|
||||
|
||||
results = self.connector.parse_isbn_search_data(search_results)
|
||||
formatted = self.connector.format_isbn_search_result(results[0])
|
||||
|
||||
self.assertEqual(formatted.title, "L'homme aux cercles bleus")
|
||||
self.assertEqual(
|
||||
formatted.key,
|
||||
"https://inventaire.io?action=by-uris&uris=isbn:9782290349229",
|
||||
)
|
||||
self.assertEqual(
|
||||
formatted.cover,
|
||||
"https://covers.inventaire.io/img/entities/12345",
|
||||
)
|
||||
|
||||
def test_get_language_code(self):
|
||||
"""get english or whatever is in reach"""
|
||||
options = {
|
||||
"de": "bip",
|
||||
"en": "hi",
|
||||
"fr": "there",
|
||||
}
|
||||
self.assertEqual(get_language_code(options), "hi")
|
||||
|
||||
options = {
|
||||
"fr": "there",
|
||||
}
|
||||
self.assertEqual(get_language_code(options), "there")
|
||||
self.assertIsNone(get_language_code({}))
|
|
@ -84,11 +84,11 @@ class SelfConnector(TestCase):
|
|||
title="Edition 1 Title", parent_work=work
|
||||
)
|
||||
edition_2 = models.Edition.objects.create(
|
||||
title="Edition 2 Title", parent_work=work
|
||||
title="Edition 2 Title",
|
||||
parent_work=work,
|
||||
edition_rank=20, # that's default babey
|
||||
)
|
||||
edition_3 = models.Edition.objects.create(title="Fish", parent_work=work)
|
||||
work.default_edition = edition_2
|
||||
work.save()
|
||||
|
||||
# pick the best edition
|
||||
results = self.connector.search("Edition 1 Title")
|
||||
|
|
45
bookwyrm/tests/data/inventaire_edition.json
Normal file
45
bookwyrm/tests/data/inventaire_edition.json
Normal file
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"entities": {
|
||||
"isbn:9780375757853": {
|
||||
"_id": "7beee121a8d9ac345cdf4e9128577723",
|
||||
"_rev": "2-ac318b04b953ca3894deb77fee28211c",
|
||||
"type": "edition",
|
||||
"labels": {},
|
||||
"claims": {
|
||||
"wdt:P31": [
|
||||
"wd:Q3331189"
|
||||
],
|
||||
"wdt:P212": [
|
||||
"978-0-375-75785-3"
|
||||
],
|
||||
"wdt:P957": [
|
||||
"0-375-75785-6"
|
||||
],
|
||||
"wdt:P407": [
|
||||
"wd:Q1860"
|
||||
],
|
||||
"wdt:P1476": [
|
||||
"The Moonstone"
|
||||
],
|
||||
"wdt:P577": [
|
||||
"2001"
|
||||
],
|
||||
"wdt:P629": [
|
||||
"wd:Q2362563"
|
||||
],
|
||||
"invp:P2": [
|
||||
"d46a8eac7555afa479b8bbb5149f35858e8e19c4"
|
||||
]
|
||||
},
|
||||
"created": 1495452670475,
|
||||
"updated": 1541032981834,
|
||||
"version": 3,
|
||||
"uri": "isbn:9780375757853",
|
||||
"originalLang": "en",
|
||||
"image": {
|
||||
"url": "/img/entities/d46a8eac7555afa479b8bbb5149f35858e8e19c4"
|
||||
}
|
||||
}
|
||||
},
|
||||
"redirects": {}
|
||||
}
|
48
bookwyrm/tests/data/inventaire_isbn_search.json
Normal file
48
bookwyrm/tests/data/inventaire_isbn_search.json
Normal file
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"entities": {
|
||||
"isbn:9782290349229": {
|
||||
"_id": "d59e3e64f92c6340fbb10c5dcf7c0abf",
|
||||
"_rev": "3-079ed51158a001dc74caafb21cff1c22",
|
||||
"type": "edition",
|
||||
"labels": {},
|
||||
"claims": {
|
||||
"wdt:P31": [
|
||||
"wd:Q3331189"
|
||||
],
|
||||
"wdt:P212": [
|
||||
"978-2-290-34922-9"
|
||||
],
|
||||
"wdt:P957": [
|
||||
"2-290-34922-4"
|
||||
],
|
||||
"wdt:P407": [
|
||||
"wd:Q150"
|
||||
],
|
||||
"wdt:P1476": [
|
||||
"L'homme aux cercles bleus"
|
||||
],
|
||||
"wdt:P629": [
|
||||
"wd:Q3203603"
|
||||
],
|
||||
"wdt:P123": [
|
||||
"wd:Q3156592"
|
||||
],
|
||||
"invp:P2": [
|
||||
"57883743aa7c6ad25885a63e6e94349ec4f71562"
|
||||
],
|
||||
"wdt:P577": [
|
||||
"2005-05-01"
|
||||
]
|
||||
},
|
||||
"created": 1485023383338,
|
||||
"updated": 1609171008418,
|
||||
"version": 5,
|
||||
"uri": "isbn:9782290349229",
|
||||
"originalLang": "fr",
|
||||
"image": {
|
||||
"url": "/img/entities/12345"
|
||||
}
|
||||
}
|
||||
},
|
||||
"redirects": {}
|
||||
}
|
111
bookwyrm/tests/data/inventaire_search.json
Normal file
111
bookwyrm/tests/data/inventaire_search.json
Normal file
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"results": [
|
||||
{
|
||||
"id": "Q7766679",
|
||||
"type": "works",
|
||||
"uri": "wd:Q7766679",
|
||||
"label": "The Stories of Vladimir Nabokov",
|
||||
"description": "book by Vladimir Nabokov",
|
||||
"image": [
|
||||
"ddb32e115a28dcc0465023869ba19f6868ec4042"
|
||||
],
|
||||
"_score": 25.180836,
|
||||
"_popularity": 4
|
||||
},
|
||||
{
|
||||
"id": "Q47407212",
|
||||
"type": "works",
|
||||
"uri": "wd:Q47407212",
|
||||
"label": "Conversations with Vladimir Nabokov",
|
||||
"description": "book edited by Robert Golla",
|
||||
"image": [],
|
||||
"_score": 24.41498,
|
||||
"_popularity": 2
|
||||
},
|
||||
{
|
||||
"id": "Q6956987",
|
||||
"type": "works",
|
||||
"uri": "wd:Q6956987",
|
||||
"label": "Nabokov's Congeries",
|
||||
"description": "book by Vladimir Nabokov",
|
||||
"image": [],
|
||||
"_score": 22.343866,
|
||||
"_popularity": 2
|
||||
},
|
||||
{
|
||||
"id": "Q6956986",
|
||||
"type": "works",
|
||||
"uri": "wd:Q6956986",
|
||||
"label": "Nabokov's Butterflies",
|
||||
"description": "book by Brian Boyd",
|
||||
"image": [],
|
||||
"_score": 22.343866,
|
||||
"_popularity": 2
|
||||
},
|
||||
{
|
||||
"id": "Q47472170",
|
||||
"type": "works",
|
||||
"uri": "wd:Q47472170",
|
||||
"label": "A Reader's Guide to Nabokov's \"Lolita\"",
|
||||
"description": "book by Julian W. Connolly",
|
||||
"image": [],
|
||||
"_score": 19.482553,
|
||||
"_popularity": 2
|
||||
},
|
||||
{
|
||||
"id": "Q7936323",
|
||||
"type": "works",
|
||||
"uri": "wd:Q7936323",
|
||||
"label": "Visiting Mrs Nabokov: And Other Excursions",
|
||||
"description": "book by Martin Amis",
|
||||
"image": [],
|
||||
"_score": 18.684965,
|
||||
"_popularity": 2
|
||||
},
|
||||
{
|
||||
"id": "1732d81bf7376e04da27568a778561a4",
|
||||
"type": "works",
|
||||
"uri": "inv:1732d81bf7376e04da27568a778561a4",
|
||||
"label": "Nabokov's Dark Cinema",
|
||||
"image": [
|
||||
"7512805a53da569b11bf29cc3fb272c969619749"
|
||||
],
|
||||
"_score": 16.56681,
|
||||
"_popularity": 1
|
||||
},
|
||||
{
|
||||
"id": "00f118336b02219e1bddc8fa93c56050",
|
||||
"type": "works",
|
||||
"uri": "inv:00f118336b02219e1bddc8fa93c56050",
|
||||
"label": "The Cambridge Companion to Nabokov",
|
||||
"image": [
|
||||
"0683a059fb95430cfa73334f9eff2ef377f3ae3d"
|
||||
],
|
||||
"_score": 15.502292,
|
||||
"_popularity": 1
|
||||
},
|
||||
{
|
||||
"id": "6e59f968a1cd00dbedeb1964dec47507",
|
||||
"type": "works",
|
||||
"uri": "inv:6e59f968a1cd00dbedeb1964dec47507",
|
||||
"label": "Vladimir Nabokov : selected letters, 1940-1977",
|
||||
"image": [
|
||||
"e3ce8c0ee89d576adf2651a6e5ce55fc6d9f8cb3"
|
||||
],
|
||||
"_score": 15.019735,
|
||||
"_popularity": 1
|
||||
},
|
||||
{
|
||||
"id": "Q127149",
|
||||
"type": "works",
|
||||
"uri": "wd:Q127149",
|
||||
"label": "Lolita",
|
||||
"description": "novel by Vladimir Nabokov",
|
||||
"image": [
|
||||
"51cbfdbf7257b1a6bb3ea3fbb167dbce1fb44a0e"
|
||||
],
|
||||
"_score": 13.458428,
|
||||
"_popularity": 32
|
||||
}
|
||||
]
|
||||
}
|
155
bookwyrm/tests/data/inventaire_work.json
Normal file
155
bookwyrm/tests/data/inventaire_work.json
Normal file
|
@ -0,0 +1,155 @@
|
|||
{
|
||||
"entities": {
|
||||
"wd:Q2362563": {
|
||||
"type": "work",
|
||||
"labels": {
|
||||
"zh-hans": "月亮宝石",
|
||||
"zh-hant": "月亮寶石",
|
||||
"zh-hk": "月光石",
|
||||
"zh-tw": "月光石",
|
||||
"cy": "The Moonstone",
|
||||
"ml": "ദ മൂൺസ്റ്റോൺ",
|
||||
"ja": "月長石",
|
||||
"te": "ది మూన్ స్టోన్",
|
||||
"ru": "Лунный камень",
|
||||
"fr": "La Pierre de lune",
|
||||
"en": "The Moonstone",
|
||||
"es": "La piedra lunar",
|
||||
"it": "La Pietra di Luna",
|
||||
"zh": "月亮宝石",
|
||||
"pl": "Kamień Księżycowy",
|
||||
"sr": "2 Јн",
|
||||
"ta": "moon stone",
|
||||
"ar": "حجر القمر",
|
||||
"fa": "ماهالماس",
|
||||
"uk": "Місячний камінь",
|
||||
"nl": "The Moonstone",
|
||||
"de": "Der Monddiamant",
|
||||
"sl": "Diamant",
|
||||
"sv": "Månstenen",
|
||||
"he": "אבן הירח",
|
||||
"eu": "Ilargi-harriak",
|
||||
"bg": "Лунният камък",
|
||||
"ka": "მთვარის ქვა",
|
||||
"eo": "La Lunŝtono",
|
||||
"hy": "Լուսնաքար",
|
||||
"ro": "Piatra Lunii",
|
||||
"ca": "The Moonstone",
|
||||
"is": "The Moonstone"
|
||||
},
|
||||
"descriptions": {
|
||||
"it": "romanzo scritto da Wilkie Collins",
|
||||
"en": "novel by Wilkie Collins",
|
||||
"de": "Buch von Wilkie Collins",
|
||||
"nl": "boek van Wilkie Collins",
|
||||
"ru": "роман Уилки Коллинза",
|
||||
"he": "רומן מאת וילקי קולינס",
|
||||
"ar": "رواية من تأليف ويلكي كولينز",
|
||||
"fr": "livre de Wilkie Collins",
|
||||
"es": "libro de Wilkie Collins",
|
||||
"bg": "роман на Уилки Колинс",
|
||||
"ka": "უილკი კოლინსის რომანი",
|
||||
"eo": "angalingva romano far Wilkie Collins",
|
||||
"ro": "roman de Wilkie Collins"
|
||||
},
|
||||
"aliases": {
|
||||
"zh": [
|
||||
"月光石"
|
||||
],
|
||||
"ml": [
|
||||
"The Moonstone"
|
||||
],
|
||||
"fr": [
|
||||
"The Moonstone"
|
||||
],
|
||||
"it": [
|
||||
"Il diamante indiano",
|
||||
"La pietra della luna",
|
||||
"La maledizione del diamante indiano"
|
||||
],
|
||||
"ro": [
|
||||
"The Moonstone"
|
||||
]
|
||||
},
|
||||
"claims": {
|
||||
"wdt:P18": [
|
||||
"The Moonstone 1st ed.jpg"
|
||||
],
|
||||
"wdt:P31": [
|
||||
"wd:Q7725634"
|
||||
],
|
||||
"wdt:P50": [
|
||||
"wd:Q210740"
|
||||
],
|
||||
"wdt:P123": [
|
||||
"wd:Q4457856"
|
||||
],
|
||||
"wdt:P136": [
|
||||
"wd:Q465821",
|
||||
"wd:Q208505",
|
||||
"wd:Q10992055"
|
||||
],
|
||||
"wdt:P156": [
|
||||
"wd:Q7228798"
|
||||
],
|
||||
"wdt:P268": [
|
||||
"12496407z"
|
||||
],
|
||||
"wdt:P407": [
|
||||
"wd:Q7979"
|
||||
],
|
||||
"wdt:P577": [
|
||||
"1868"
|
||||
],
|
||||
"wdt:P1433": [
|
||||
"wd:Q21"
|
||||
],
|
||||
"wdt:P1476": [
|
||||
"The Moonstone"
|
||||
],
|
||||
"wdt:P1680": [
|
||||
"A Romance"
|
||||
],
|
||||
"wdt:P2034": [
|
||||
"155"
|
||||
]
|
||||
},
|
||||
"sitelinks": {
|
||||
"arwiki": "حجر القمر (رواية)",
|
||||
"bgwiki": "Лунният камък (роман)",
|
||||
"cywiki": "The Moonstone",
|
||||
"dewiki": "Der Monddiamant",
|
||||
"enwiki": "The Moonstone",
|
||||
"enwikisource": "The Moonstone",
|
||||
"eswiki": "La piedra lunar",
|
||||
"euwiki": "Ilargi-harria",
|
||||
"fawiki": "ماهالماس",
|
||||
"frwiki": "La Pierre de lune (roman de Wilkie Collins)",
|
||||
"hewiki": "אבן הירח",
|
||||
"hywiki": "Լուսնաքար",
|
||||
"iswiki": "The Moonstone",
|
||||
"itwiki": "La pietra di Luna",
|
||||
"jawiki": "月長石 (小説)",
|
||||
"mlwiki": "ദ മൂൺസ്റ്റോൺ",
|
||||
"plwiki": "Kamień Księżycowy (powieść)",
|
||||
"ruwiki": "Лунный камень (роман)",
|
||||
"slwiki": "Diamant (roman)",
|
||||
"srwikisource": "Нови завјет (Караџић) / 2. Јованова",
|
||||
"svwiki": "Månstenen",
|
||||
"tewiki": "ది మూన్స్టోన్",
|
||||
"ukwiki": "Місячний камінь (роман)",
|
||||
"zhwiki": "月亮宝石"
|
||||
},
|
||||
"uri": "wd:Q2362563",
|
||||
"image": {
|
||||
"url": "https://commons.wikimedia.org/wiki/Special:FilePath/The%20Moonstone%201st%20ed.jpg?width=1000",
|
||||
"file": "The Moonstone 1st ed.jpg",
|
||||
"credits": {
|
||||
"text": "Wikimedia Commons",
|
||||
"url": "https://commons.wikimedia.org/wiki/File:The Moonstone 1st ed.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"redirects": {}
|
||||
}
|
|
@ -84,9 +84,3 @@ class Book(TestCase):
|
|||
self.first_edition.description = "hi"
|
||||
self.first_edition.save()
|
||||
self.assertEqual(self.first_edition.edition_rank, 1)
|
||||
|
||||
# default edition
|
||||
self.work.default_edition = self.first_edition
|
||||
self.work.save()
|
||||
self.first_edition.refresh_from_db()
|
||||
self.assertEqual(self.first_edition.edition_rank, 20)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from bookwyrm import models, settings
|
||||
from bookwyrm import models
|
||||
|
||||
|
||||
class ReadThrough(TestCase):
|
||||
|
@ -19,8 +19,6 @@ class ReadThrough(TestCase):
|
|||
self.edition = models.Edition.objects.create(
|
||||
title="Example Edition", parent_work=self.work
|
||||
)
|
||||
self.work.default_edition = self.edition
|
||||
self.work.save()
|
||||
|
||||
self.readthrough = models.ReadThrough.objects.create(
|
||||
user=self.user, book=self.edition
|
||||
|
|
|
@ -35,29 +35,49 @@ class InboxAdd(TestCase):
|
|||
work = models.Work.objects.create(title="work title")
|
||||
self.book = models.Edition.objects.create(
|
||||
title="Test",
|
||||
remote_id="https://bookwyrm.social/book/37292",
|
||||
remote_id="https://example.com/book/37292",
|
||||
parent_work=work,
|
||||
)
|
||||
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
@responses.activate
|
||||
def test_handle_add_book_to_shelf(self):
|
||||
"""shelving a book"""
|
||||
shelf = models.Shelf.objects.create(user=self.remote_user, name="Test Shelf")
|
||||
shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read"
|
||||
shelf.remote_id = "https://example.com/user/rat/shelf/to-read"
|
||||
shelf.save()
|
||||
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://example.com/user/rat/shelf/to-read",
|
||||
json={
|
||||
"id": shelf.remote_id,
|
||||
"type": "Shelf",
|
||||
"totalItems": 1,
|
||||
"first": "https://example.com/shelf/22?page=1",
|
||||
"last": "https://example.com/shelf/22?page=1",
|
||||
"name": "Test Shelf",
|
||||
"owner": self.remote_user.remote_id,
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc": ["https://example.com/user/rat/followers"],
|
||||
"summary": "summary text",
|
||||
"curation": "curated",
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
},
|
||||
)
|
||||
|
||||
activity = {
|
||||
"id": "https://bookwyrm.social/shelfbook/6189#add",
|
||||
"id": "https://example.com/shelfbook/6189#add",
|
||||
"type": "Add",
|
||||
"actor": "https://example.com/users/rat",
|
||||
"object": {
|
||||
"actor": self.remote_user.remote_id,
|
||||
"type": "ShelfItem",
|
||||
"book": self.book.remote_id,
|
||||
"id": "https://bookwyrm.social/shelfbook/6189",
|
||||
"id": "https://example.com/shelfbook/6189",
|
||||
},
|
||||
"target": "https://bookwyrm.social/user/mouse/shelf/to-read",
|
||||
"target": "https://example.com/user/rat/shelf/to-read",
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
}
|
||||
views.inbox.activity_task(activity)
|
||||
|
@ -68,7 +88,7 @@ class InboxAdd(TestCase):
|
|||
"""listing a book"""
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://bookwyrm.social/user/mouse/list/to-read",
|
||||
"https://example.com/user/mouse/list/to-read",
|
||||
json={
|
||||
"id": "https://example.com/list/22",
|
||||
"type": "BookList",
|
||||
|
@ -86,17 +106,17 @@ class InboxAdd(TestCase):
|
|||
)
|
||||
|
||||
activity = {
|
||||
"id": "https://bookwyrm.social/listbook/6189#add",
|
||||
"id": "https://example.com/listbook/6189#add",
|
||||
"type": "Add",
|
||||
"actor": "https://example.com/users/rat",
|
||||
"object": {
|
||||
"actor": self.remote_user.remote_id,
|
||||
"type": "ListItem",
|
||||
"book": self.book.remote_id,
|
||||
"id": "https://bookwyrm.social/listbook/6189",
|
||||
"id": "https://example.com/listbook/6189",
|
||||
"order": 1,
|
||||
},
|
||||
"target": "https://bookwyrm.social/user/mouse/list/to-read",
|
||||
"target": "https://example.com/user/mouse/list/to-read",
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
}
|
||||
views.inbox.activity_task(activity)
|
||||
|
@ -105,4 +125,4 @@ class InboxAdd(TestCase):
|
|||
listitem = models.ListItem.objects.get()
|
||||
self.assertEqual(booklist.name, "Test List")
|
||||
self.assertEqual(booklist.books.first(), self.book)
|
||||
self.assertEqual(listitem.remote_id, "https://bookwyrm.social/listbook/6189")
|
||||
self.assertEqual(listitem.remote_id, "https://example.com/listbook/6189")
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue