Merge branch 'main' into list-style-fixes

This commit is contained in:
Joachim 2021-05-05 16:06:48 +02:00 committed by GitHub
commit 1f747e4f68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
126 changed files with 5790 additions and 9540 deletions

View file

@ -7,6 +7,9 @@ DEBUG=true
DOMAIN=your.domain.here
#EMAIL=your@email.here
# Used for deciding which editions to prefer
DEFAULT_LANGUAGE="English"
## Leave unset to allow all hosts
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"

View file

@ -7,6 +7,9 @@ DEBUG=false
DOMAIN=your.domain.here
EMAIL=your@email.here
# Used for deciding which editions to prefer
DEFAULT_LANGUAGE="English"
## Leave unset to allow all hosts
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -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,

View 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

View file

@ -14,8 +14,8 @@ class Connector(AbstractConnector):
def __init__(self, identifier):
super().__init__(identifier)
get_first = lambda a: a[0]
get_remote_id = lambda a: self.base_url + a
get_first = lambda a, *args: a[0]
get_remote_id = lambda a, *args: self.base_url + a
self.book_mappings = [
Mapping("title"),
Mapping("id", remote_field="key", formatter=get_remote_id),

View file

@ -3,7 +3,7 @@ from functools import reduce
import operator
from django.contrib.postgres.search import SearchRank, SearchVector
from django.db.models import Count, F, Q
from django.db.models import Count, OuterRef, Subquery, F, Q
from bookwyrm import models
from .abstract_connector import AbstractConnector, SearchResult
@ -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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,

View file

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

View file

@ -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

View file

@ -11,6 +11,7 @@ DOMAIN = env("DOMAIN")
VERSION = "0.0.1"
PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
# celery
CELERY_BROKER = env("CELERY_BROKER")

View file

@ -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 bulmas named breapoints:
*
* `is-(w|h)-(auto|xs|s|m|l|xl|xxl)[-(mobile|tablet|desktop)]`
*
* The cover will be centered horizontally and vertically within those dimensions.
*
* When using `.column.is-N`, add `.is-w-auto` to the container so that the flex
* calculations are not biased by the default `max-content`.
******************************************************************************/
.column.is-cover {
flex-grow: 0 !important;
}
.column.is-cover,
.column.is-cover + .column {
flex-basis: auto !important;
}
.cover-container {
height: 250px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
width: max-content;
max-width: 250px;
max-width: 100%;
overflow: hidden;
}
.cover-container.is-large {
height: max-content;
max-width: 330px;
}
.cover-container.is-large img {
max-height: 500px;
height: auto;
}
.cover-container.is-medium {
height: 150px;
}
.cover-container.is-small {
height: 100px;
}
@media only screen and (max-width: 768px) {
.cover-container {
height: 200px;
width: max-content;
}
.cover-container.is-medium {
height: 100px;
}
}
/* Book cover
* -------------------------------------------------------------------------- */
.book-cover {
height: 100%;
object-fit: scale-down;
display: block;
max-width: 100%;
max-height: 100%;
/* Useful when stretching under-sized images. */
image-rendering: optimizeQuality;
image-rendering: smooth;
}
.no-cover {
position: relative;
white-space: normal;
}
/* Cover caption
* -------------------------------------------------------------------------- */
.no-cover div {
.no-cover .cover_caption {
position: absolute;
padding: 1em;
color: white;
top: 0;
right: 0;
bottom: 0;
left: 0;
text-align: center;
}
.cover-container.is-medium .no-cover div {
font-size: 0.9em;
padding: 0.3em;
}
.cover-container.is-small .no-cover div {
font-size: 0.7em;
padding: 0.1em;
padding: 0.25em;
font-size: 0.75em;
color: white;
background-color: #002549;
}
/** Avatars
@ -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 Bulmas. They follow the same conventions.
* Add those youll need.
******************************************************************************/
.mr-auto {
margin-right: auto !important;
}
.ml-auto {
margin-left: auto !important;
}
@media screen and (max-width: 768px) {
.m-0-mobile {
margin: 0 !important;
}
.mr-auto-mobile {
margin-right: auto !important;
}
.ml-auto-mobile {
margin-left: auto !important;
}
.mt-3-mobile {
margin-top: 0.75rem !important;
}
.ml-3-mobile {
margin-left: 0.75rem !important;
}
.mx-3-mobile {
margin-right: 0.75rem !important;
margin-left: 0.75rem !important;
}
.my-3-mobile {
margin-top: 0.75rem !important;
margin-bottom: 0.75rem !important;
}
}
@media screen and (min-width: 769px) {
.m-0-tablet {
margin: 0 !important;
}
.mr-auto-tablet {
margin-right: auto !important;
}
.ml-auto-tablet {
margin-left: auto !important;
}
.mt-3-tablet {
margin-top: 0.75rem !important;
}
.ml-3-tablet {
margin-left: 0.75rem !important;
}
.mx-3-tablet {
margin-right: 0.75rem !important;
margin-left: 0.75rem !important;
}
.my-3-tablet {
margin-top: 0.75rem !important;
margin-bottom: 0.75rem !important;
}
}

File diff suppressed because one or more lines are too long

View file

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

View file

@ -39,3 +39,6 @@
</div>
{% endblock %}
{% block scripts %}
{% include 'snippets/datepicker_js.html' %}
{% endblock %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

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

View file

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

View 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 %}&nbsp;{% endif %}
</div>
</div>
<footer class="card-footer">
{% if user != request.user %}
{% if user.mutuals %}
<div class="card-footer-item">
<div class="has-text-centered">
<p class="title is-6 mb-0">{{ user.mutuals }}</p>
<p class="help">{% blocktrans count counter=user.mutuals %}follower you follow{% plural %}followers you follow{% endblocktrans %}</p>
</div>
</div>
{% elif user.shared_books %}
<div class="card-footer-item">
<div class="has-text-centered">
<p class="title is-6 mb-0">{{ user.shared_books }}</p>
<p class="help">{% blocktrans count counter=user.shared_books %}book on your shelves{% plural %}books on your shelves{% endblocktrans %}</p>
</div>
</div>
{% endif %}
{% endif %}
<div class="card-footer-item">
<div class="has-text-centered">
<p class="title is-6 mb-0">{{ user.status_set.count|intword }}</p>
<p class="help">{% trans "posts" %}</p>
</div>
</div>
<div class="card-footer-item">
<div class="has-text-centered">
<p class="title is-6 mb-0">{{ user.last_active_date|naturalday }}</p>
<p class="help">{% trans "last active" %}</p>
</div>
</div>
</footer>
</div>

View file

@ -1,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 %}

View file

@ -1,12 +1,24 @@
{% load bookwyrm_tags %}
{% load i18n %}
{% if book %}
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
{% 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 %}

View file

@ -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 %}

View file

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

View file

@ -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 %}

View file

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

View file

@ -23,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 &amp; continue" %}</button>
</form>
{% endblock %}
{% block scripts %}
{% include 'snippets/datepicker_js.html' %}
{% endblock %}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 %}

View file

@ -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>

View file

@ -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 %}

View file

@ -28,7 +28,7 @@
</div>
{% if lists %}
<section class="block content">
<section class="block">
{% include 'lists/list_items.html' with lists=lists %}
</section>

View file

@ -33,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>

View file

@ -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 %}

View file

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

View file

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

View file

@ -0,0 +1,78 @@
{% extends 'search/layout.html' %}
{% load i18n %}
{% block panel %}
{% if results %}
{% with results|first as local_results %}
<ul class="block">
{% for result in local_results.results %}
<li class="pd-4">
{% 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 %}

View file

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

View file

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

View file

@ -0,0 +1,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 %}

View file

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

View file

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

View file

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

View file

@ -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 %}

View file

@ -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>

View file

@ -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>

View 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" />

View file

@ -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">&hellip;</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>

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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 %}

View file

@ -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>

View file

@ -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>

View file

@ -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" %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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>

View 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 %}

View 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 %}

View 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 %}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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()

View file

@ -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",

View 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({}))

View file

@ -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")

View 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": {}
}

View 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": {}
}

View 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
}
]
}

View 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": {}
}

View file

@ -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)

View file

@ -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

View file

@ -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