forked from mirrors/bookwyrm
Merge branch 'main' into groups-merge-test
Big merge of a couple of weeks' work from the main project back into this branch. :ohno:
This commit is contained in:
commit
602664b8d7
231 changed files with 8983 additions and 7361 deletions
2
.github/workflows/curlylint.yaml
vendored
2
.github/workflows/curlylint.yaml
vendored
|
@ -24,5 +24,5 @@ jobs:
|
||||||
--rule 'meta_viewport: true' \
|
--rule 'meta_viewport: true' \
|
||||||
--rule 'no_autofocus: true' \
|
--rule 'no_autofocus: true' \
|
||||||
--rule 'tabindex_no_positive: true' \
|
--rule 'tabindex_no_positive: true' \
|
||||||
--exclude '_modal.html|create_status/layout.html' \
|
--exclude '_modal.html|create_status/layout.html|reading_modals/layout.html' \
|
||||||
bookwyrm/templates
|
bookwyrm/templates
|
||||||
|
|
|
@ -8,4 +8,4 @@ WORKDIR /app
|
||||||
|
|
||||||
COPY requirements.txt /app/
|
COPY requirements.txt /app/
|
||||||
RUN pip install -r requirements.txt --no-cache-dir
|
RUN pip install -r requirements.txt --no-cache-dir
|
||||||
RUN apt-get update && apt-get install -y gettext libgettextpo-dev && apt-get clean
|
RUN apt-get update && apt-get install -y gettext libgettextpo-dev tidy && apt-get clean
|
||||||
|
|
|
@ -54,6 +54,7 @@ class Edition(Book):
|
||||||
asin: str = ""
|
asin: str = ""
|
||||||
pages: int = None
|
pages: int = None
|
||||||
physicalFormat: str = ""
|
physicalFormat: str = ""
|
||||||
|
physicalFormatDetail: str = ""
|
||||||
publishers: List[str] = field(default_factory=lambda: [])
|
publishers: List[str] = field(default_factory=lambda: [])
|
||||||
editionRank: int = 0
|
editionRank: int = 0
|
||||||
|
|
||||||
|
|
156
bookwyrm/book_search.py
Normal file
156
bookwyrm/book_search.py
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
""" using a bookwyrm instance as a source of book data """
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from functools import reduce
|
||||||
|
import operator
|
||||||
|
|
||||||
|
from django.contrib.postgres.search import SearchRank, SearchQuery
|
||||||
|
from django.db.models import OuterRef, Subquery, F, Q
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm.settings import MEDIA_FULL_URL
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=arguments-differ
|
||||||
|
def search(query, min_confidence=0, filters=None, return_first=False):
|
||||||
|
"""search your local database"""
|
||||||
|
filters = filters or []
|
||||||
|
if not query:
|
||||||
|
return []
|
||||||
|
# first, try searching unqiue identifiers
|
||||||
|
results = search_identifiers(query, *filters, return_first=return_first)
|
||||||
|
if not results:
|
||||||
|
# then try searching title/author
|
||||||
|
results = search_title_author(
|
||||||
|
query, min_confidence, *filters, return_first=return_first
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def isbn_search(query):
|
||||||
|
"""search your local database"""
|
||||||
|
if not query:
|
||||||
|
return []
|
||||||
|
|
||||||
|
filters = [{f: query} for f in ["isbn_10", "isbn_13"]]
|
||||||
|
results = models.Edition.objects.filter(
|
||||||
|
reduce(operator.or_, (Q(**f) for f in filters))
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
# when there are multiple editions of the same work, pick the default.
|
||||||
|
# it would be odd for this to happen.
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def format_search_result(search_result):
|
||||||
|
"""convert a book object into a search result object"""
|
||||||
|
cover = None
|
||||||
|
if search_result.cover:
|
||||||
|
cover = f"{MEDIA_FULL_URL}{search_result.cover}"
|
||||||
|
|
||||||
|
return SearchResult(
|
||||||
|
title=search_result.title,
|
||||||
|
key=search_result.remote_id,
|
||||||
|
author=search_result.author_text,
|
||||||
|
year=search_result.published_date.year
|
||||||
|
if search_result.published_date
|
||||||
|
else None,
|
||||||
|
cover=cover,
|
||||||
|
confidence=search_result.rank if hasattr(search_result, "rank") else 1,
|
||||||
|
connector="",
|
||||||
|
).json()
|
||||||
|
|
||||||
|
|
||||||
|
def search_identifiers(query, *filters, return_first=False):
|
||||||
|
"""tries remote_id, isbn; defined as dedupe fields on the model"""
|
||||||
|
# pylint: disable=W0212
|
||||||
|
or_filters = [
|
||||||
|
{f.name: query}
|
||||||
|
for f in models.Edition._meta.get_fields()
|
||||||
|
if hasattr(f, "deduplication_field") and f.deduplication_field
|
||||||
|
]
|
||||||
|
results = models.Edition.objects.filter(
|
||||||
|
*filters, reduce(operator.or_, (Q(**f) for f in or_filters))
|
||||||
|
).distinct()
|
||||||
|
if results.count() <= 1:
|
||||||
|
return results
|
||||||
|
|
||||||
|
# when there are multiple editions of the same work, pick the default.
|
||||||
|
# it would be odd for this to happen.
|
||||||
|
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
|
||||||
|
)
|
||||||
|
if return_first:
|
||||||
|
return results.first()
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def search_title_author(query, min_confidence, *filters, return_first=False):
|
||||||
|
"""searches for title and author"""
|
||||||
|
query = SearchQuery(query, config="simple") | SearchQuery(query, config="english")
|
||||||
|
results = (
|
||||||
|
models.Edition.objects.filter(*filters, search_vector=query)
|
||||||
|
.annotate(rank=SearchRank(F("search_vector"), query))
|
||||||
|
.filter(rank__gt=min_confidence)
|
||||||
|
.order_by("-rank")
|
||||||
|
)
|
||||||
|
|
||||||
|
# when there are multiple editions of the same work, pick the closest
|
||||||
|
editions_of_work = results.values("parent_work__id").values_list("parent_work__id")
|
||||||
|
|
||||||
|
# filter out multiple editions of the same work
|
||||||
|
list_results = []
|
||||||
|
for work_id in set(editions_of_work):
|
||||||
|
editions = results.filter(parent_work=work_id)
|
||||||
|
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:
|
||||||
|
result = default
|
||||||
|
else:
|
||||||
|
result = editions.first()
|
||||||
|
if return_first:
|
||||||
|
return result
|
||||||
|
list_results.append(result)
|
||||||
|
return list_results
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SearchResult:
|
||||||
|
"""standardized search result object"""
|
||||||
|
|
||||||
|
title: str
|
||||||
|
key: str
|
||||||
|
connector: object
|
||||||
|
view_link: str = None
|
||||||
|
author: str = None
|
||||||
|
year: str = None
|
||||||
|
cover: str = None
|
||||||
|
confidence: int = 1
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
# pylint: disable=consider-using-f-string
|
||||||
|
return "<SearchResult key={!r} title={!r} author={!r}>".format(
|
||||||
|
self.key, self.title, self.author
|
||||||
|
)
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
"""serialize a connector for json response"""
|
||||||
|
serialized = asdict(self)
|
||||||
|
del serialized["connector"]
|
||||||
|
return serialized
|
|
@ -3,4 +3,4 @@ from .settings import CONNECTORS
|
||||||
from .abstract_connector import ConnectorException
|
from .abstract_connector import ConnectorException
|
||||||
from .abstract_connector import get_data, get_image
|
from .abstract_connector import get_data, get_image
|
||||||
|
|
||||||
from .connector_manager import search, local_search, first_search_result
|
from .connector_manager import search, first_search_result
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
""" functionality outline for a book data connector """
|
""" functionality outline for a book data connector """
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import asdict, dataclass
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
@ -9,6 +8,7 @@ from requests.exceptions import RequestException
|
||||||
|
|
||||||
from bookwyrm import activitypub, models, settings
|
from bookwyrm import activitypub, models, settings
|
||||||
from .connector_manager import load_more_data, ConnectorException
|
from .connector_manager import load_more_data, ConnectorException
|
||||||
|
from .format_mappings import format_mappings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -31,7 +31,6 @@ class AbstractMinimalConnector(ABC):
|
||||||
"isbn_search_url",
|
"isbn_search_url",
|
||||||
"name",
|
"name",
|
||||||
"identifier",
|
"identifier",
|
||||||
"local",
|
|
||||||
]
|
]
|
||||||
for field in self_fields:
|
for field in self_fields:
|
||||||
setattr(self, field, getattr(info, field))
|
setattr(self, field, getattr(info, field))
|
||||||
|
@ -267,32 +266,6 @@ def get_image(url, timeout=10):
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SearchResult:
|
|
||||||
"""standardized search result object"""
|
|
||||||
|
|
||||||
title: str
|
|
||||||
key: str
|
|
||||||
connector: object
|
|
||||||
view_link: str = None
|
|
||||||
author: str = None
|
|
||||||
year: str = None
|
|
||||||
cover: str = None
|
|
||||||
confidence: int = 1
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
# pylint: disable=consider-using-f-string
|
|
||||||
return "<SearchResult key={!r} title={!r} author={!r}>".format(
|
|
||||||
self.key, self.title, self.author
|
|
||||||
)
|
|
||||||
|
|
||||||
def json(self):
|
|
||||||
"""serialize a connector for json response"""
|
|
||||||
serialized = asdict(self)
|
|
||||||
del serialized["connector"]
|
|
||||||
return serialized
|
|
||||||
|
|
||||||
|
|
||||||
class Mapping:
|
class Mapping:
|
||||||
"""associate a local database field with a field in an external dataset"""
|
"""associate a local database field with a field in an external dataset"""
|
||||||
|
|
||||||
|
@ -312,3 +285,25 @@ class Mapping:
|
||||||
return self.formatter(value)
|
return self.formatter(value)
|
||||||
except: # pylint: disable=bare-except
|
except: # pylint: disable=bare-except
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def infer_physical_format(format_text):
|
||||||
|
"""try to figure out what the standardized format is from the free value"""
|
||||||
|
format_text = format_text.lower()
|
||||||
|
if format_text in format_mappings:
|
||||||
|
# try a direct match
|
||||||
|
return format_mappings[format_text]
|
||||||
|
# failing that, try substring
|
||||||
|
matches = [v for k, v in format_mappings.items() if k in format_text]
|
||||||
|
if not matches:
|
||||||
|
return None
|
||||||
|
return matches[0]
|
||||||
|
|
||||||
|
|
||||||
|
def unique_physical_format(format_text):
|
||||||
|
"""only store the format if it isn't diretly in the format mappings"""
|
||||||
|
format_text = format_text.lower()
|
||||||
|
if format_text in format_mappings:
|
||||||
|
# try a direct match, so saving this would be redundant
|
||||||
|
return None
|
||||||
|
return format_text
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
""" using another bookwyrm instance as a source of book data """
|
""" using another bookwyrm instance as a source of book data """
|
||||||
from bookwyrm import activitypub, models
|
from bookwyrm import activitypub, models
|
||||||
from .abstract_connector import AbstractMinimalConnector, SearchResult
|
from bookwyrm.book_search import SearchResult
|
||||||
|
from .abstract_connector import AbstractMinimalConnector
|
||||||
|
|
||||||
|
|
||||||
class Connector(AbstractMinimalConnector):
|
class Connector(AbstractMinimalConnector):
|
||||||
|
|
|
@ -10,7 +10,7 @@ from django.db.models import signals
|
||||||
|
|
||||||
from requests import HTTPError
|
from requests import HTTPError
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import book_search, models
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -55,7 +55,7 @@ def search(query, min_confidence=0.1, return_first=False):
|
||||||
# if we found anything, return it
|
# if we found anything, return it
|
||||||
return result_set[0]
|
return result_set[0]
|
||||||
|
|
||||||
if result_set or connector.local:
|
if result_set:
|
||||||
results.append(
|
results.append(
|
||||||
{
|
{
|
||||||
"connector": connector,
|
"connector": connector,
|
||||||
|
@ -71,22 +71,13 @@ def search(query, min_confidence=0.1, return_first=False):
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def local_search(query, min_confidence=0.1, raw=False, filters=None):
|
|
||||||
"""only look at local search results"""
|
|
||||||
connector = load_connector(models.Connector.objects.get(local=True))
|
|
||||||
return connector.search(
|
|
||||||
query, min_confidence=min_confidence, raw=raw, filters=filters
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def isbn_local_search(query, raw=False):
|
|
||||||
"""only look at local search results"""
|
|
||||||
connector = load_connector(models.Connector.objects.get(local=True))
|
|
||||||
return connector.isbn_search(query, raw=raw)
|
|
||||||
|
|
||||||
|
|
||||||
def first_search_result(query, min_confidence=0.1):
|
def first_search_result(query, min_confidence=0.1):
|
||||||
"""search until you find a result that fits"""
|
"""search until you find a result that fits"""
|
||||||
|
# try local search first
|
||||||
|
result = book_search.search(query, min_confidence=min_confidence, return_first=True)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
# otherwise, try remote endpoints
|
||||||
return search(query, min_confidence=min_confidence, return_first=True) or None
|
return search(query, min_confidence=min_confidence, return_first=True) or None
|
||||||
|
|
||||||
|
|
||||||
|
|
43
bookwyrm/connectors/format_mappings.py
Normal file
43
bookwyrm/connectors/format_mappings.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
""" comparing a free text format to the standardized one """
|
||||||
|
format_mappings = {
|
||||||
|
"paperback": "Paperback",
|
||||||
|
"soft": "Paperback",
|
||||||
|
"pamphlet": "Paperback",
|
||||||
|
"peperback": "Paperback",
|
||||||
|
"tapa blanda": "Paperback",
|
||||||
|
"turtleback": "Paperback",
|
||||||
|
"pocket": "Paperback",
|
||||||
|
"spiral": "Paperback",
|
||||||
|
"ring": "Paperback",
|
||||||
|
"平装": "Paperback",
|
||||||
|
"简装": "Paperback",
|
||||||
|
"hardcover": "Hardcover",
|
||||||
|
"hardcocer": "Hardcover",
|
||||||
|
"hardover": "Hardcover",
|
||||||
|
"hardback": "Hardcover",
|
||||||
|
"library": "Hardcover",
|
||||||
|
"tapa dura": "Hardcover",
|
||||||
|
"leather": "Hardcover",
|
||||||
|
"clothbound": "Hardcover",
|
||||||
|
"精装": "Hardcover",
|
||||||
|
"ebook": "EBook",
|
||||||
|
"e-book": "EBook",
|
||||||
|
"digital": "EBook",
|
||||||
|
"computer file": "EBook",
|
||||||
|
"epub": "EBook",
|
||||||
|
"online": "EBook",
|
||||||
|
"pdf": "EBook",
|
||||||
|
"elektronische": "EBook",
|
||||||
|
"electronic": "EBook",
|
||||||
|
"audiobook": "AudiobookFormat",
|
||||||
|
"audio": "AudiobookFormat",
|
||||||
|
"cd": "AudiobookFormat",
|
||||||
|
"dvd": "AudiobookFormat",
|
||||||
|
"mp3": "AudiobookFormat",
|
||||||
|
"cassette": "AudiobookFormat",
|
||||||
|
"kindle": "AudiobookFormat",
|
||||||
|
"talking": "AudiobookFormat",
|
||||||
|
"sound": "AudiobookFormat",
|
||||||
|
"comic": "GraphicNovel",
|
||||||
|
"graphic": "GraphicNovel",
|
||||||
|
}
|
|
@ -2,13 +2,14 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from .abstract_connector import AbstractConnector, SearchResult, Mapping
|
from bookwyrm.book_search import SearchResult
|
||||||
|
from .abstract_connector import AbstractConnector, Mapping
|
||||||
from .abstract_connector import get_data
|
from .abstract_connector import get_data
|
||||||
from .connector_manager import ConnectorException
|
from .connector_manager import ConnectorException
|
||||||
|
|
||||||
|
|
||||||
class Connector(AbstractConnector):
|
class Connector(AbstractConnector):
|
||||||
"""instantiate a connector for OL"""
|
"""instantiate a connector for inventaire"""
|
||||||
|
|
||||||
def __init__(self, identifier):
|
def __init__(self, identifier):
|
||||||
super().__init__(identifier)
|
super().__init__(identifier)
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from .abstract_connector import AbstractConnector, SearchResult, Mapping
|
from bookwyrm.book_search import SearchResult
|
||||||
from .abstract_connector import get_data
|
from .abstract_connector import AbstractConnector, Mapping
|
||||||
|
from .abstract_connector import get_data, infer_physical_format, unique_physical_format
|
||||||
from .connector_manager import ConnectorException
|
from .connector_manager import ConnectorException
|
||||||
from .openlibrary_languages import languages
|
from .openlibrary_languages import languages
|
||||||
|
|
||||||
|
@ -43,7 +44,16 @@ class Connector(AbstractConnector):
|
||||||
),
|
),
|
||||||
Mapping("publishedDate", remote_field="publish_date"),
|
Mapping("publishedDate", remote_field="publish_date"),
|
||||||
Mapping("pages", remote_field="number_of_pages"),
|
Mapping("pages", remote_field="number_of_pages"),
|
||||||
Mapping("physicalFormat", remote_field="physical_format"),
|
Mapping(
|
||||||
|
"physicalFormat",
|
||||||
|
remote_field="physical_format",
|
||||||
|
formatter=infer_physical_format,
|
||||||
|
),
|
||||||
|
Mapping(
|
||||||
|
"physicalFormatDetail",
|
||||||
|
remote_field="physical_format",
|
||||||
|
formatter=unique_physical_format,
|
||||||
|
),
|
||||||
Mapping("publishers"),
|
Mapping("publishers"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,164 +0,0 @@
|
||||||
""" using a bookwyrm instance as a source of book data """
|
|
||||||
from functools import reduce
|
|
||||||
import operator
|
|
||||||
|
|
||||||
from django.contrib.postgres.search import SearchRank, SearchQuery
|
|
||||||
from django.db.models import OuterRef, Subquery, F, Q
|
|
||||||
|
|
||||||
from bookwyrm import models
|
|
||||||
from .abstract_connector import AbstractConnector, SearchResult
|
|
||||||
|
|
||||||
|
|
||||||
class Connector(AbstractConnector):
|
|
||||||
"""instantiate a connector"""
|
|
||||||
|
|
||||||
# pylint: disable=arguments-differ
|
|
||||||
def search(self, query, min_confidence=0, raw=False, filters=None):
|
|
||||||
"""search your local database"""
|
|
||||||
filters = filters or []
|
|
||||||
if not query:
|
|
||||||
return []
|
|
||||||
# first, try searching unqiue identifiers
|
|
||||||
results = search_identifiers(query, *filters)
|
|
||||||
if not results:
|
|
||||||
# then try searching title/author
|
|
||||||
results = search_title_author(query, min_confidence, *filters)
|
|
||||||
search_results = []
|
|
||||||
for result in results:
|
|
||||||
if raw:
|
|
||||||
search_results.append(result)
|
|
||||||
else:
|
|
||||||
search_results.append(self.format_search_result(result))
|
|
||||||
if len(search_results) >= 10:
|
|
||||||
break
|
|
||||||
if not raw:
|
|
||||||
search_results.sort(key=lambda r: r.confidence, reverse=True)
|
|
||||||
return search_results
|
|
||||||
|
|
||||||
def isbn_search(self, query, raw=False):
|
|
||||||
"""search your local database"""
|
|
||||||
if not query:
|
|
||||||
return []
|
|
||||||
|
|
||||||
filters = [{f: query} for f in ["isbn_10", "isbn_13"]]
|
|
||||||
results = models.Edition.objects.filter(
|
|
||||||
reduce(operator.or_, (Q(**f) for f in filters))
|
|
||||||
).distinct()
|
|
||||||
|
|
||||||
# when there are multiple editions of the same work, pick the default.
|
|
||||||
# it would be odd for this to happen.
|
|
||||||
|
|
||||||
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:
|
|
||||||
if raw:
|
|
||||||
search_results.append(result)
|
|
||||||
else:
|
|
||||||
search_results.append(self.format_search_result(result))
|
|
||||||
if len(search_results) >= 10:
|
|
||||||
break
|
|
||||||
return search_results
|
|
||||||
|
|
||||||
def format_search_result(self, search_result):
|
|
||||||
cover = None
|
|
||||||
if search_result.cover:
|
|
||||||
cover = f"{self.covers_url}{search_result.cover}"
|
|
||||||
|
|
||||||
return SearchResult(
|
|
||||||
title=search_result.title,
|
|
||||||
key=search_result.remote_id,
|
|
||||||
author=search_result.author_text,
|
|
||||||
year=search_result.published_date.year
|
|
||||||
if search_result.published_date
|
|
||||||
else None,
|
|
||||||
connector=self,
|
|
||||||
cover=cover,
|
|
||||||
confidence=search_result.rank if hasattr(search_result, "rank") else 1,
|
|
||||||
)
|
|
||||||
|
|
||||||
def format_isbn_search_result(self, search_result):
|
|
||||||
return self.format_search_result(search_result)
|
|
||||||
|
|
||||||
def is_work_data(self, data):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_edition_from_work_data(self, data):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_work_from_edition_data(self, data):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_authors_from_data(self, data):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def parse_isbn_search_data(self, data):
|
|
||||||
"""it's already in the right format, don't even worry about it"""
|
|
||||||
return data
|
|
||||||
|
|
||||||
def parse_search_data(self, data):
|
|
||||||
"""it's already in the right format, don't even worry about it"""
|
|
||||||
return data
|
|
||||||
|
|
||||||
def expand_book_data(self, book):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def search_identifiers(query, *filters):
|
|
||||||
"""tries remote_id, isbn; defined as dedupe fields on the model"""
|
|
||||||
# pylint: disable=W0212
|
|
||||||
or_filters = [
|
|
||||||
{f.name: query}
|
|
||||||
for f in models.Edition._meta.get_fields()
|
|
||||||
if hasattr(f, "deduplication_field") and f.deduplication_field
|
|
||||||
]
|
|
||||||
results = models.Edition.objects.filter(
|
|
||||||
*filters, reduce(operator.or_, (Q(**f) for f in or_filters))
|
|
||||||
).distinct()
|
|
||||||
if results.count() <= 1:
|
|
||||||
return results
|
|
||||||
|
|
||||||
# when there are multiple editions of the same work, pick the default.
|
|
||||||
# it would be odd for this to happen.
|
|
||||||
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):
|
|
||||||
"""searches for title and author"""
|
|
||||||
query = SearchQuery(query, config="simple") | SearchQuery(query, config="english")
|
|
||||||
results = (
|
|
||||||
models.Edition.objects.filter(*filters, search_vector=query)
|
|
||||||
.annotate(rank=SearchRank(F("search_vector"), query))
|
|
||||||
.filter(rank__gt=min_confidence)
|
|
||||||
.order_by("-rank")
|
|
||||||
)
|
|
||||||
|
|
||||||
# when there are multiple editions of the same work, pick the closest
|
|
||||||
editions_of_work = results.values("parent_work__id").values_list("parent_work__id")
|
|
||||||
|
|
||||||
# filter out multiple editions of the same work
|
|
||||||
for work_id in set(editions_of_work):
|
|
||||||
editions = results.filter(parent_work=work_id)
|
|
||||||
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
|
|
||||||
else:
|
|
||||||
yield editions.first()
|
|
|
@ -1,3 +1,3 @@
|
||||||
""" settings book data connectors """
|
""" settings book data connectors """
|
||||||
|
|
||||||
CONNECTORS = ["openlibrary", "inventaire", "self_connector", "bookwyrm_connector"]
|
CONNECTORS = ["openlibrary", "inventaire", "bookwyrm_connector"]
|
||||||
|
|
|
@ -29,8 +29,7 @@ class CustomForm(ModelForm):
|
||||||
input_type = visible.field.widget.input_type
|
input_type = visible.field.widget.input_type
|
||||||
if isinstance(visible.field.widget, Textarea):
|
if isinstance(visible.field.widget, Textarea):
|
||||||
input_type = "textarea"
|
input_type = "textarea"
|
||||||
visible.field.widget.attrs["cols"] = None
|
visible.field.widget.attrs["rows"] = 5
|
||||||
visible.field.widget.attrs["rows"] = None
|
|
||||||
visible.field.widget.attrs["class"] = css_classes[input_type]
|
visible.field.widget.attrs["class"] = css_classes[input_type]
|
||||||
|
|
||||||
|
|
||||||
|
@ -228,7 +227,7 @@ class ExpiryWidget(widgets.Select):
|
||||||
elif selected_string == "forever":
|
elif selected_string == "forever":
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return selected_string # "This will raise
|
return selected_string # This will raise
|
||||||
|
|
||||||
return timezone.now() + interval
|
return timezone.now() + interval
|
||||||
|
|
||||||
|
@ -269,7 +268,7 @@ class CreateInviteForm(CustomForm):
|
||||||
class ShelfForm(CustomForm):
|
class ShelfForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Shelf
|
model = models.Shelf
|
||||||
fields = ["user", "name", "privacy"]
|
fields = ["user", "name", "privacy", "description"]
|
||||||
|
|
||||||
|
|
||||||
class GoalForm(CustomForm):
|
class GoalForm(CustomForm):
|
||||||
|
|
|
@ -4,7 +4,6 @@ from django.contrib.auth.models import Group, Permission
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from bookwyrm.models import Connector, FederatedServer, SiteSettings, User
|
from bookwyrm.models import Connector, FederatedServer, SiteSettings, User
|
||||||
from bookwyrm.settings import DOMAIN
|
|
||||||
|
|
||||||
|
|
||||||
def init_groups():
|
def init_groups():
|
||||||
|
@ -73,19 +72,6 @@ def init_permissions():
|
||||||
|
|
||||||
def init_connectors():
|
def init_connectors():
|
||||||
"""access book data sources"""
|
"""access book data sources"""
|
||||||
Connector.objects.create(
|
|
||||||
identifier=DOMAIN,
|
|
||||||
name="Local",
|
|
||||||
local=True,
|
|
||||||
connector_file="self_connector",
|
|
||||||
base_url="https://%s" % DOMAIN,
|
|
||||||
books_url="https://%s/book" % DOMAIN,
|
|
||||||
covers_url="https://%s/images/" % DOMAIN,
|
|
||||||
search_url="https://%s/search?q=" % DOMAIN,
|
|
||||||
isbn_search_url="https://%s/isbn/" % DOMAIN,
|
|
||||||
priority=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
Connector.objects.create(
|
Connector.objects.create(
|
||||||
identifier="bookwyrm.social",
|
identifier="bookwyrm.social",
|
||||||
name="BookWyrm dot Social",
|
name="BookWyrm dot Social",
|
||||||
|
|
37
bookwyrm/migrations/0099_readthrough_is_active.py
Normal file
37
bookwyrm/migrations/0099_readthrough_is_active.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Generated by Django 3.2.4 on 2021-09-22 16:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def set_active_readthrough(apps, schema_editor):
|
||||||
|
"""best-guess for deactivation date"""
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
apps.get_model("bookwyrm", "ReadThrough").objects.using(db_alias).filter(
|
||||||
|
start_date__isnull=False,
|
||||||
|
finish_date__isnull=True,
|
||||||
|
).update(is_active=True)
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_func(apps, schema_editor):
|
||||||
|
"""noop"""
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0098_auto_20210918_2238"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="readthrough",
|
||||||
|
name="is_active",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.RunPython(set_active_readthrough, reverse_func),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="readthrough",
|
||||||
|
name="is_active",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
18
bookwyrm/migrations/0100_shelf_description.py
Normal file
18
bookwyrm/migrations/0100_shelf_description.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.5 on 2021-09-28 23:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0099_readthrough_is_active"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="shelf",
|
||||||
|
name="description",
|
||||||
|
field=models.TextField(blank=True, max_length=500, null=True),
|
||||||
|
),
|
||||||
|
]
|
56
bookwyrm/migrations/0101_auto_20210929_1847.py
Normal file
56
bookwyrm/migrations/0101_auto_20210929_1847.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
# Generated by Django 3.2 on 2021-05-21 00:17
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import bookwyrm
|
||||||
|
from bookwyrm.connectors.abstract_connector import infer_physical_format
|
||||||
|
|
||||||
|
|
||||||
|
def infer_format(app_registry, schema_editor):
|
||||||
|
"""set the new phsyical format field based on existing format data"""
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
editions = (
|
||||||
|
app_registry.get_model("bookwyrm", "Edition")
|
||||||
|
.objects.using(db_alias)
|
||||||
|
.filter(physical_format_detail__isnull=False)
|
||||||
|
)
|
||||||
|
for edition in editions:
|
||||||
|
free_format = edition.physical_format_detail.lower()
|
||||||
|
edition.physical_format = infer_physical_format(free_format)
|
||||||
|
edition.save()
|
||||||
|
|
||||||
|
|
||||||
|
def reverse(app_registry, schema_editor):
|
||||||
|
"""doesn't need to do anything"""
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0100_shelf_description"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="edition",
|
||||||
|
old_name="physical_format",
|
||||||
|
new_name="physical_format_detail",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="edition",
|
||||||
|
name="physical_format",
|
||||||
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("AudiobookFormat", "Audiobook"),
|
||||||
|
("EBook", "eBook"),
|
||||||
|
("GraphicNovel", "Graphic novel"),
|
||||||
|
("Hardcover", "Hardcover"),
|
||||||
|
("Paperback", "Paperback"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(infer_format, reverse),
|
||||||
|
]
|
41
bookwyrm/migrations/0102_remove_connector_local.py
Normal file
41
bookwyrm/migrations/0102_remove_connector_local.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Generated by Django 3.2.5 on 2021-09-30 17:46
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from bookwyrm.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
def remove_self_connector(app_registry, schema_editor):
|
||||||
|
"""set the new phsyical format field based on existing format data"""
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
app_registry.get_model("bookwyrm", "Connector").objects.using(db_alias).filter(
|
||||||
|
connector_file="self_connector"
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
|
||||||
|
def reverse(app_registry, schema_editor):
|
||||||
|
"""doesn't need to do anything"""
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
model = app_registry.get_model("bookwyrm", "Connector")
|
||||||
|
model.objects.using(db_alias).create(
|
||||||
|
identifier=DOMAIN,
|
||||||
|
name="Local",
|
||||||
|
local=True,
|
||||||
|
connector_file="self_connector",
|
||||||
|
base_url=f"https://{DOMAIN}",
|
||||||
|
books_url=f"https://{DOMAIN}/book",
|
||||||
|
covers_url=f"https://{DOMAIN}/images/",
|
||||||
|
search_url=f"https://{DOMAIN}/search?q=",
|
||||||
|
isbn_search_url=f"https://{DOMAIN}/isbn/",
|
||||||
|
priority=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0101_auto_20210929_1847"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(remove_self_connector, reverse),
|
||||||
|
]
|
17
bookwyrm/migrations/0103_remove_connector_local.py
Normal file
17
bookwyrm/migrations/0103_remove_connector_local.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 3.2.5 on 2021-09-30 18:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0102_remove_connector_local"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="connector",
|
||||||
|
name="local",
|
||||||
|
),
|
||||||
|
]
|
53
bookwyrm/migrations/0104_auto_20211001_2012.py
Normal file
53
bookwyrm/migrations/0104_auto_20211001_2012.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# Generated by Django 3.2.5 on 2021-10-01 20:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def set_thread_id(app_registry, schema_editor):
|
||||||
|
"""set thread ids"""
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
# set the thread id on parent nodes
|
||||||
|
model = app_registry.get_model("bookwyrm", "Status")
|
||||||
|
model.objects.using(db_alias).filter(reply_parent__isnull=True).update(
|
||||||
|
thread_id=models.F("id")
|
||||||
|
)
|
||||||
|
|
||||||
|
queryset = model.objects.using(db_alias).filter(
|
||||||
|
reply_parent__isnull=False,
|
||||||
|
reply_parent__thread_id__isnull=False,
|
||||||
|
thread_id__isnull=True,
|
||||||
|
)
|
||||||
|
iters = 0
|
||||||
|
while queryset.exists():
|
||||||
|
queryset.update(
|
||||||
|
thread_id=models.Subquery(
|
||||||
|
model.objects.filter(id=models.OuterRef("reply_parent")).values_list(
|
||||||
|
"thread_id"
|
||||||
|
)[:1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(iters)
|
||||||
|
iters += 1
|
||||||
|
if iters > 50:
|
||||||
|
print("exceeded query depth")
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def reverse(*_):
|
||||||
|
"""do nothing"""
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0103_remove_connector_local"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="status",
|
||||||
|
name="thread_id",
|
||||||
|
field=models.IntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.RunPython(set_thread_id, reverse),
|
||||||
|
]
|
|
@ -1,8 +1,11 @@
|
||||||
""" base model with default fields """
|
""" base model with default fields """
|
||||||
import base64
|
import base64
|
||||||
from Crypto import Random
|
from Crypto import Random
|
||||||
|
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from django.http import Http404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
|
@ -48,26 +51,26 @@ class BookWyrmModel(models.Model):
|
||||||
"""how to link to this object in the local app"""
|
"""how to link to this object in the local app"""
|
||||||
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
|
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
|
||||||
|
|
||||||
def visible_to_user(self, viewer):
|
def raise_visible_to_user(self, viewer):
|
||||||
"""is a user authorized to view an object?"""
|
"""is a user authorized to view an object?"""
|
||||||
# make sure this is an object with privacy owned by a user
|
# make sure this is an object with privacy owned by a user
|
||||||
if not hasattr(self, "user") or not hasattr(self, "privacy"):
|
if not hasattr(self, "user") or not hasattr(self, "privacy"):
|
||||||
return None
|
return
|
||||||
|
|
||||||
# viewer can't see it if the object's owner blocked them
|
# viewer can't see it if the object's owner blocked them
|
||||||
if viewer in self.user.blocks.all():
|
if viewer in self.user.blocks.all():
|
||||||
return False
|
raise Http404()
|
||||||
|
|
||||||
# you can see your own posts and any public or unlisted posts
|
# you can see your own posts and any public or unlisted posts
|
||||||
if viewer == self.user or self.privacy in ["public", "unlisted"]:
|
if viewer == self.user or self.privacy in ["public", "unlisted"]:
|
||||||
return True
|
return
|
||||||
|
|
||||||
# you can see the followers only posts of people you follow
|
# you can see the followers only posts of people you follow
|
||||||
if (
|
if (
|
||||||
self.privacy == "followers"
|
self.privacy == "followers"
|
||||||
and self.user.followers.filter(id=viewer.id).first()
|
and self.user.followers.filter(id=viewer.id).first()
|
||||||
):
|
):
|
||||||
return True
|
return
|
||||||
|
|
||||||
# you can see dms you are tagged in
|
# you can see dms you are tagged in
|
||||||
if hasattr(self, "mention_users"):
|
if hasattr(self, "mention_users"):
|
||||||
|
@ -75,6 +78,7 @@ class BookWyrmModel(models.Model):
|
||||||
self.privacy == "direct"
|
self.privacy == "direct"
|
||||||
and self.mention_users.filter(id=viewer.id).first()
|
and self.mention_users.filter(id=viewer.id).first()
|
||||||
):
|
):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# you can see groups of which you are a member
|
# you can see groups of which you are a member
|
||||||
|
@ -89,7 +93,31 @@ class BookWyrmModel(models.Model):
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
raise Http404()
|
||||||
|
|
||||||
|
def raise_not_editable(self, viewer):
|
||||||
|
"""does this user have permission to edit this object? liable to be overwritten
|
||||||
|
by models that inherit this base model class"""
|
||||||
|
if not hasattr(self, "user"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# generally moderators shouldn't be able to edit other people's stuff
|
||||||
|
if self.user == viewer:
|
||||||
|
return
|
||||||
|
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
def raise_not_deletable(self, viewer):
|
||||||
|
"""does this user have permission to delete this object? liable to be
|
||||||
|
overwritten by models that inherit this base model class"""
|
||||||
|
if not hasattr(self, "user"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# but generally moderators can delete other people's stuff
|
||||||
|
if self.user == viewer or viewer.has_perm("moderate_post"):
|
||||||
|
return
|
||||||
|
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
|
||||||
@receiver(models.signals.post_save)
|
@receiver(models.signals.post_save)
|
||||||
|
|
|
@ -3,9 +3,10 @@ import re
|
||||||
|
|
||||||
from django.contrib.postgres.search import SearchVectorField
|
from django.contrib.postgres.search import SearchVectorField
|
||||||
from django.contrib.postgres.indexes import GinIndex
|
from django.contrib.postgres.indexes import GinIndex
|
||||||
from django.db import models
|
from django.db import models, transaction
|
||||||
from django.db import transaction
|
from django.db.models import Prefetch
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from model_utils import FieldTracker
|
from model_utils import FieldTracker
|
||||||
from model_utils.managers import InheritanceManager
|
from model_utils.managers import InheritanceManager
|
||||||
from imagekit.models import ImageSpecField
|
from imagekit.models import ImageSpecField
|
||||||
|
@ -226,6 +227,16 @@ class Work(OrderedCollectionPageMixin, Book):
|
||||||
deserialize_reverse_fields = [("editions", "editions")]
|
deserialize_reverse_fields = [("editions", "editions")]
|
||||||
|
|
||||||
|
|
||||||
|
# https://schema.org/BookFormatType
|
||||||
|
FormatChoices = [
|
||||||
|
("AudiobookFormat", _("Audiobook")),
|
||||||
|
("EBook", _("eBook")),
|
||||||
|
("GraphicNovel", _("Graphic novel")),
|
||||||
|
("Hardcover", _("Hardcover")),
|
||||||
|
("Paperback", _("Paperback")),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class Edition(Book):
|
class Edition(Book):
|
||||||
"""an edition of a book"""
|
"""an edition of a book"""
|
||||||
|
|
||||||
|
@ -243,7 +254,10 @@ class Edition(Book):
|
||||||
max_length=255, blank=True, null=True, deduplication_field=True
|
max_length=255, blank=True, null=True, deduplication_field=True
|
||||||
)
|
)
|
||||||
pages = fields.IntegerField(blank=True, null=True)
|
pages = fields.IntegerField(blank=True, null=True)
|
||||||
physical_format = fields.CharField(max_length=255, blank=True, null=True)
|
physical_format = fields.CharField(
|
||||||
|
max_length=255, choices=FormatChoices, null=True, blank=True
|
||||||
|
)
|
||||||
|
physical_format_detail = fields.CharField(max_length=255, blank=True, null=True)
|
||||||
publishers = fields.ArrayField(
|
publishers = fields.ArrayField(
|
||||||
models.CharField(max_length=255), blank=True, default=list
|
models.CharField(max_length=255), blank=True, default=list
|
||||||
)
|
)
|
||||||
|
@ -307,6 +321,27 @@ class Edition(Book):
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def viewer_aware_objects(cls, viewer):
|
||||||
|
"""annotate a book query with metadata related to the user"""
|
||||||
|
queryset = cls.objects
|
||||||
|
if not viewer or not viewer.is_authenticated:
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
queryset = queryset.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"shelfbook_set",
|
||||||
|
queryset=viewer.shelfbook_set.all(),
|
||||||
|
to_attr="current_shelves",
|
||||||
|
),
|
||||||
|
Prefetch(
|
||||||
|
"readthrough_set",
|
||||||
|
queryset=viewer.readthrough_set.filter(is_active=True).all(),
|
||||||
|
to_attr="active_readthroughs",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
def isbn_10_to_13(isbn_10):
|
def isbn_10_to_13(isbn_10):
|
||||||
"""convert an isbn 10 into an isbn 13"""
|
"""convert an isbn 10 into an isbn 13"""
|
||||||
|
|
|
@ -14,7 +14,6 @@ class Connector(BookWyrmModel):
|
||||||
identifier = models.CharField(max_length=255, unique=True)
|
identifier = models.CharField(max_length=255, unique=True)
|
||||||
priority = models.IntegerField(default=2)
|
priority = models.IntegerField(default=2)
|
||||||
name = models.CharField(max_length=255, null=True, blank=True)
|
name = models.CharField(max_length=255, null=True, blank=True)
|
||||||
local = models.BooleanField(default=False)
|
|
||||||
connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)
|
connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)
|
||||||
api_key = models.CharField(max_length=255, null=True, blank=True)
|
api_key = models.CharField(max_length=255, null=True, blank=True)
|
||||||
active = models.BooleanField(default=True)
|
active = models.BooleanField(default=True)
|
||||||
|
|
|
@ -101,6 +101,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
|
||||||
notification_type="ADD",
|
notification_type="ADD",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
if self.book_list.group:
|
if self.book_list.group:
|
||||||
for membership in self.book_list.group.memberships.all():
|
for membership in self.book_list.group.memberships.all():
|
||||||
if membership.user != self.user:
|
if membership.user != self.user:
|
||||||
|
@ -110,6 +111,14 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
|
||||||
related_list_item=self,
|
related_list_item=self,
|
||||||
notification_type="ADD"
|
notification_type="ADD"
|
||||||
)
|
)
|
||||||
|
=======
|
||||||
|
def raise_not_deletable(self, viewer):
|
||||||
|
"""the associated user OR the list owner can delete"""
|
||||||
|
if self.book_list.user == viewer:
|
||||||
|
return
|
||||||
|
super().raise_not_deletable(viewer)
|
||||||
|
|
||||||
|
>>>>>>> main
|
||||||
class Meta:
|
class Meta:
|
||||||
"""A book may only be placed into a list once,
|
"""A book may only be placed into a list once,
|
||||||
and each order in the list may be used only once"""
|
and each order in the list may be used only once"""
|
||||||
|
|
|
@ -26,10 +26,14 @@ class ReadThrough(BookWyrmModel):
|
||||||
)
|
)
|
||||||
start_date = models.DateTimeField(blank=True, null=True)
|
start_date = models.DateTimeField(blank=True, null=True)
|
||||||
finish_date = models.DateTimeField(blank=True, null=True)
|
finish_date = models.DateTimeField(blank=True, null=True)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""update user active time"""
|
"""update user active time"""
|
||||||
self.user.update_active_date()
|
self.user.update_active_date()
|
||||||
|
# an active readthrough must have an unset finish date
|
||||||
|
if self.finish_date:
|
||||||
|
self.is_active = False
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def create_update(self):
|
def create_update(self):
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
""" puttin' books on shelves """
|
""" puttin' books on shelves """
|
||||||
import re
|
import re
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||||
|
|
||||||
name = fields.CharField(max_length=100)
|
name = fields.CharField(max_length=100)
|
||||||
identifier = models.CharField(max_length=100)
|
identifier = models.CharField(max_length=100)
|
||||||
|
description = models.TextField(blank=True, null=True, max_length=500)
|
||||||
user = fields.ForeignKey(
|
user = fields.ForeignKey(
|
||||||
"User", on_delete=models.PROTECT, activitypub_field="owner"
|
"User", on_delete=models.PROTECT, activitypub_field="owner"
|
||||||
)
|
)
|
||||||
|
@ -51,12 +53,23 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||||
"""list of books for this shelf, overrides OrderedCollectionMixin"""
|
"""list of books for this shelf, overrides OrderedCollectionMixin"""
|
||||||
return self.books.order_by("shelfbook")
|
return self.books.order_by("shelfbook")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def deletable(self):
|
||||||
|
"""can the shelf be safely deleted?"""
|
||||||
|
return self.editable and not self.shelfbook_set.exists()
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""shelf identifier instead of id"""
|
"""shelf identifier instead of id"""
|
||||||
base_path = self.user.remote_id
|
base_path = self.user.remote_id
|
||||||
identifier = self.identifier or self.get_identifier()
|
identifier = self.identifier or self.get_identifier()
|
||||||
return f"{base_path}/books/{identifier}"
|
return f"{base_path}/books/{identifier}"
|
||||||
|
|
||||||
|
def raise_not_deletable(self, viewer):
|
||||||
|
"""don't let anyone delete a default shelf"""
|
||||||
|
super().raise_not_deletable(viewer)
|
||||||
|
if not self.deletable:
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""user/shelf unqiueness"""
|
"""user/shelf unqiueness"""
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ from dataclasses import MISSING
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
@ -56,6 +57,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
activitypub_field="inReplyTo",
|
activitypub_field="inReplyTo",
|
||||||
)
|
)
|
||||||
|
thread_id = models.IntegerField(blank=True, null=True)
|
||||||
objects = InheritanceManager()
|
objects = InheritanceManager()
|
||||||
|
|
||||||
activity_serializer = activitypub.Note
|
activity_serializer = activitypub.Note
|
||||||
|
@ -67,6 +69,17 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
|
|
||||||
ordering = ("-published_date",)
|
ordering = ("-published_date",)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""save and notify"""
|
||||||
|
if self.reply_parent:
|
||||||
|
self.thread_id = self.reply_parent.thread_id or self.reply_parent.id
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
if not self.reply_parent:
|
||||||
|
self.thread_id = self.id
|
||||||
|
super().save(broadcast=False, update_fields=["thread_id"])
|
||||||
|
|
||||||
def delete(self, *args, **kwargs): # pylint: disable=unused-argument
|
def delete(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||||
""" "delete" a status"""
|
""" "delete" a status"""
|
||||||
if hasattr(self, "boosted_status"):
|
if hasattr(self, "boosted_status"):
|
||||||
|
@ -187,6 +200,13 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
"""json serialized activitypub class"""
|
"""json serialized activitypub class"""
|
||||||
return self.to_activity_dataclass(pure=pure).serialize()
|
return self.to_activity_dataclass(pure=pure).serialize()
|
||||||
|
|
||||||
|
def raise_not_editable(self, viewer):
|
||||||
|
"""certain types of status aren't editable"""
|
||||||
|
# first, the standard raise
|
||||||
|
super().raise_not_editable(viewer)
|
||||||
|
if isinstance(self, (GeneratedNote, ReviewRating)):
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
|
||||||
class GeneratedNote(Status):
|
class GeneratedNote(Status):
|
||||||
"""these are app-generated messages about user activity"""
|
"""these are app-generated messages about user activity"""
|
||||||
|
|
|
@ -13,7 +13,7 @@ VERSION = "0.0.1"
|
||||||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||||
|
|
||||||
JS_CACHE = "7f2343cf"
|
JS_CACHE = "c02929b1"
|
||||||
|
|
||||||
# email
|
# email
|
||||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||||
|
|
|
@ -492,6 +492,23 @@ ol.ordered-list li::before {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Threads
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
.thread .is-main .card {
|
||||||
|
box-shadow: 0 0.5em 1em -0.125em rgb(50 115 220 / 35%), 0 0 0 1px rgb(50 115 220 / 2%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 2.5em;
|
||||||
|
border-left: 2px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dimensions
|
/* Dimensions
|
||||||
* @todo These could be in rem.
|
* @todo These could be in rem.
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
21
bookwyrm/static/js/block_href.js
Normal file
21
bookwyrm/static/js/block_href.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/* exported BlockHref */
|
||||||
|
|
||||||
|
let BlockHref = new class {
|
||||||
|
constructor() {
|
||||||
|
document.querySelectorAll('[data-href]')
|
||||||
|
.forEach(t => t.addEventListener('click', this.followLink.bind(this)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Follow a fake link
|
||||||
|
*
|
||||||
|
* @param {Event} event
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
followLink(event) {
|
||||||
|
const url = event.currentTarget.dataset.href;
|
||||||
|
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
|
@ -141,8 +141,10 @@ let StatusCache = new class {
|
||||||
modal.getElementsByClassName("modal-close")[0].click();
|
modal.getElementsByClassName("modal-close")[0].click();
|
||||||
|
|
||||||
// Update shelve buttons
|
// Update shelve buttons
|
||||||
|
if (form.reading_status) {
|
||||||
document.querySelectorAll("[data-shelve-button-book='" + form.book.value +"']")
|
document.querySelectorAll("[data-shelve-button-book='" + form.book.value +"']")
|
||||||
.forEach(button => this.cycleShelveButtons(button, form.reading_status.value));
|
.forEach(button => this.cycleShelveButtons(button, form.reading_status.value));
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block columns" itemscope itemtype="https://schema.org/Person">
|
<div class="block columns content" itemscope itemtype="https://schema.org/Person">
|
||||||
<meta itemprop="name" content="{{ author.name }}">
|
<meta itemprop="name" content="{{ author.name }}">
|
||||||
|
|
||||||
{% if author.aliases or author.born or author.died or author.wikipedia_link or author.openlibrary_key or author.inventaire_id %}
|
{% if author.aliases or author.born or author.died or author.wikipedia_link or author.openlibrary_key or author.inventaire_id %}
|
||||||
|
|
|
@ -203,7 +203,9 @@
|
||||||
<hr aria-hidden="true">
|
<hr aria-hidden="true">
|
||||||
|
|
||||||
<section class="box">
|
<section class="box">
|
||||||
|
{% with 0|uuid as controls_uid %}
|
||||||
{% include 'snippets/create_status.html' with book=book hide_cover=True %}
|
{% include 'snippets/create_status.html' with book=book hide_cover=True %}
|
||||||
|
{% endwith %}
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="block" id="reviews">
|
<div class="block" id="reviews">
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% if book.isbn13 or book.oclc_number or book.asin %}
|
||||||
<dl>
|
<dl>
|
||||||
{% if book.isbn_13 %}
|
{% if book.isbn_13 %}
|
||||||
<div class="is-flex">
|
<div class="is-flex">
|
||||||
|
@ -23,4 +24,5 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
|
{% endif %}
|
||||||
{% endspaceless %}
|
{% endspaceless %}
|
||||||
|
|
116
bookwyrm/templates/book/edit/edit_book.html
Normal file
116
bookwyrm/templates/book/edit/edit_book.html
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
|
{% block title %}{% if book %}{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}{% else %}{% trans "Add Book" %}{% endif %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<header class="block">
|
||||||
|
<h1 class="title level-left">
|
||||||
|
{% if book %}
|
||||||
|
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Add Book" %}
|
||||||
|
{% endif %}
|
||||||
|
</h1>
|
||||||
|
{% if book %}
|
||||||
|
<dl>
|
||||||
|
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Added:" %}</dt>
|
||||||
|
<dd class="ml-2">{{ book.created_date | naturaltime }}</dd>
|
||||||
|
|
||||||
|
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Updated:" %}</dt>
|
||||||
|
<dd class="ml-2">{{ book.updated_date | naturaltime }}</dd>
|
||||||
|
|
||||||
|
{% if book.last_edited_by %}
|
||||||
|
<dt class="is-pulled-left mr-5 has-text-weight-semibold">{% trans "Last edited by:" %}</dt>
|
||||||
|
<dd class="ml-2"><a href="{{ book.last_edited_by.remote_id }}">{{ book.last_edited_by.display_name }}</a></dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</dl>
|
||||||
|
{% endif %}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="block"
|
||||||
|
{% if book %}
|
||||||
|
name="edit-book"
|
||||||
|
action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
|
||||||
|
{% else %}
|
||||||
|
name="create-book"
|
||||||
|
action="/create-book{% if confirm_mode %}/confirm{% endif %}"
|
||||||
|
{% endif %}
|
||||||
|
method="post"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
>
|
||||||
|
{% if confirm_mode %}
|
||||||
|
<div class="box">
|
||||||
|
<h2 class="title is-4">{% trans "Confirm Book Info" %}</h2>
|
||||||
|
<div class="columns mb-4">
|
||||||
|
{% if author_matches %}
|
||||||
|
<input type="hidden" name="author-match-count" value="{{ author_matches|length }}">
|
||||||
|
<div class="column is-half">
|
||||||
|
{% for author in author_matches %}
|
||||||
|
<fieldset>
|
||||||
|
<legend class="title is-5 mb-1">
|
||||||
|
{% blocktrans with name=author.name %}Is "{{ name }}" an existing author?{% endblocktrans %}
|
||||||
|
</legend>
|
||||||
|
{% with forloop.counter0 as counter %}
|
||||||
|
{% for match in author.matches %}
|
||||||
|
<label class="label mb-2">
|
||||||
|
<input type="radio" name="author_match-{{ counter }}" value="{{ match.id }}" required>
|
||||||
|
{{ match.name }}
|
||||||
|
</label>
|
||||||
|
<p class="help">
|
||||||
|
<a href="{{ match.local_path }}" target="_blank">{% blocktrans with book_title=match.book_set.first.title %}Author of <em>{{ book_title }}</em>{% endblocktrans %}</a>
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
<label class="label">
|
||||||
|
<input type="radio" name="author_match-{{ counter }}" value="{{ author.name }}" required> {% trans "This is a new author" %}
|
||||||
|
</label>
|
||||||
|
{% endwith %}
|
||||||
|
</fieldset>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="column is-half">{% blocktrans with name=add_author %}Creating a new author: {{ name }}{% endblocktrans %}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not book %}
|
||||||
|
<div class="column is-half">
|
||||||
|
<fieldset>
|
||||||
|
<legend class="title is-5 mb-1">
|
||||||
|
{% trans "Is this an edition of an existing work?" %}
|
||||||
|
</legend>
|
||||||
|
{% for match in book_matches %}
|
||||||
|
<label class="label">
|
||||||
|
<input type="radio" name="parent_work" value="{{ match.parent_work.id }}"> {{ match.parent_work.title }}
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="parent_work" value="0" required> {% trans "This is a new work" %}
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button>
|
||||||
|
<a href="#" class="button" data-back>
|
||||||
|
<span>{% trans "Back" %}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="block">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% include "book/edit/edit_book_form.html" %}
|
||||||
|
|
||||||
|
{% if not confirm_mode %}
|
||||||
|
<div class="block">
|
||||||
|
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||||
|
<a class="button" href="{{ book.local_path }}">{% trans "Cancel" %}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -1,40 +1,4 @@
|
||||||
{% extends 'layout.html' %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load humanize %}
|
|
||||||
|
|
||||||
{% block title %}{% if book %}{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}{% else %}{% trans "Add Book" %}{% endif %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<header class="block">
|
|
||||||
<h1 class="title level-left">
|
|
||||||
{% if book %}
|
|
||||||
{% blocktrans with book_title=book.title %}Edit "{{ book_title }}"{% endblocktrans %}
|
|
||||||
{% else %}
|
|
||||||
{% trans "Add Book" %}
|
|
||||||
{% endif %}
|
|
||||||
</h1>
|
|
||||||
{% if book %}
|
|
||||||
<dl>
|
|
||||||
<div class="is-flex">
|
|
||||||
<dt class="has-text-weight-semibold">{% trans "Added:" %}</dt>
|
|
||||||
<dd class="ml-2">{{ book.created_date | naturaltime }}</dd>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="is-flex">
|
|
||||||
<dt class="has-text-weight-semibold">{% trans "Updated:" %}</dt>
|
|
||||||
<dd class="ml-2">{{ book.updated_date | naturaltime }}</dd>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if book.last_edited_by %}
|
|
||||||
<div class="is-flex">
|
|
||||||
<dt class="has-text-weight-semibold">{% trans "Last edited by:" %}</dt>
|
|
||||||
<dd class="ml-2"><a href="{{ book.last_edited_by.remote_id }}">{{ book.last_edited_by.display_name }}</a></dd>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</dl>
|
|
||||||
{% endif %}
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{% if form.non_field_errors %}
|
{% if form.non_field_errors %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
|
@ -42,87 +6,14 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form
|
{% csrf_token %}
|
||||||
class="block"
|
|
||||||
{% if book %}
|
|
||||||
name="edit-book"
|
|
||||||
action="{{ book.local_path }}/{% if confirm_mode %}confirm{% else %}edit{% endif %}"
|
|
||||||
{% else %}
|
|
||||||
name="create-book"
|
|
||||||
action="/create-book{% if confirm_mode %}/confirm{% endif %}"
|
|
||||||
{% endif %}
|
|
||||||
method="post"
|
|
||||||
enctype="multipart/form-data"
|
|
||||||
>
|
|
||||||
|
|
||||||
{% csrf_token %}
|
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
||||||
{% if confirm_mode %}
|
<div class="columns">
|
||||||
<div class="box">
|
|
||||||
<h2 class="title is-4">{% trans "Confirm Book Info" %}</h2>
|
|
||||||
<div class="columns mb-4">
|
|
||||||
{% if author_matches %}
|
|
||||||
<input type="hidden" name="author-match-count" value="{{ author_matches|length }}">
|
|
||||||
<div class="column is-half">
|
|
||||||
{% for author in author_matches %}
|
|
||||||
<fieldset>
|
|
||||||
<legend class="title is-5 mb-1">
|
|
||||||
{% blocktrans with name=author.name %}Is "{{ name }}" an existing author?{% endblocktrans %}
|
|
||||||
</legend>
|
|
||||||
{% with forloop.counter0 as counter %}
|
|
||||||
{% for match in author.matches %}
|
|
||||||
<label class="label mb-2">
|
|
||||||
<input type="radio" name="author_match-{{ counter }}" value="{{ match.id }}" required>
|
|
||||||
{{ match.name }}
|
|
||||||
</label>
|
|
||||||
<p class="help">
|
|
||||||
<a href="{{ match.local_path }}" target="_blank">{% blocktrans with book_title=match.book_set.first.title %}Author of <em>{{ book_title }}</em>{% endblocktrans %}</a>
|
|
||||||
</p>
|
|
||||||
{% endfor %}
|
|
||||||
<label class="label">
|
|
||||||
<input type="radio" name="author_match-{{ counter }}" value="{{ author.name }}" required> {% trans "This is a new author" %}
|
|
||||||
</label>
|
|
||||||
{% endwith %}
|
|
||||||
</fieldset>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p class="column is-half">{% blocktrans with name=add_author %}Creating a new author: {{ name }}{% endblocktrans %}</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if not book %}
|
|
||||||
<div class="column is-half">
|
|
||||||
<fieldset>
|
|
||||||
<legend class="title is-5 mb-1">
|
|
||||||
{% trans "Is this an edition of an existing work?" %}
|
|
||||||
</legend>
|
|
||||||
{% for match in book_matches %}
|
|
||||||
<label class="label">
|
|
||||||
<input type="radio" name="parent_work" value="{{ match.parent_work.id }}"> {{ match.parent_work.title }}
|
|
||||||
</label>
|
|
||||||
{% endfor %}
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="parent_work" value="0" required> {% trans "This is a new work" %}
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button>
|
|
||||||
<a href="#" class="button" data-back>
|
|
||||||
<span>{% trans "Back" %}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="block">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2 class="title is-4">{% trans "Metadata" %}</h2>
|
<h2 class="title is-4">{% trans "Metadata" %}</h2>
|
||||||
|
<div class="box">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_title">{% trans "Title:" %}</label>
|
<label class="label" for="id_title">{% trans "Title:" %}</label>
|
||||||
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title">
|
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title">
|
||||||
|
@ -147,6 +38,8 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-two-thirds">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_series">{% trans "Series:" %}</label>
|
<label class="label" for="id_series">{% trans "Series:" %}</label>
|
||||||
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}">
|
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}">
|
||||||
|
@ -154,7 +47,8 @@
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-one-third">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_series_number">{% trans "Series number:" %}</label>
|
<label class="label" for="id_series_number">{% trans "Series number:" %}</label>
|
||||||
{{ form.series_number }}
|
{{ form.series_number }}
|
||||||
|
@ -162,6 +56,8 @@
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_languages">{% trans "Languages:" %}</label>
|
<label class="label" for="id_languages">{% trans "Languages:" %}</label>
|
||||||
|
@ -171,7 +67,12 @@
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="block">
|
||||||
|
<h2 class="title is-4">{% trans "Publication" %}</h2>
|
||||||
|
<div class="box">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
|
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
|
||||||
{{ form.publishers }}
|
{{ form.publishers }}
|
||||||
|
@ -196,10 +97,12 @@
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<h2 class="title is-4">{% trans "Authors" %}</h2>
|
<h2 class="title is-4">{% trans "Authors" %}</h2>
|
||||||
|
<div class="box">
|
||||||
{% if book.authors.exists %}
|
{% if book.authors.exists %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{% for author in book.authors.all %}
|
{% for author in book.authors.all %}
|
||||||
|
@ -220,46 +123,65 @@
|
||||||
<input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'John Doe, Jane Smith' %}" value="{{ add_author }}" {% if confirm_mode %}readonly{% endif %}>
|
<input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'John Doe, Jane Smith' %}" value="{{ add_author }}" {% if confirm_mode %}readonly{% endif %}>
|
||||||
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
<span class="help">{% trans "Separate multiple values with commas." %}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
|
<section class="block">
|
||||||
<h2 class="title is-4">{% trans "Cover" %}</h2>
|
<h2 class="title is-4">{% trans "Cover" %}</h2>
|
||||||
|
<div class="box">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
|
{% if book.cover %}
|
||||||
<div class="column is-3 is-cover">
|
<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' size_mobile='xlarge' size='large' %}
|
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-xl-mobile is-w-auto-tablet' size_mobile='xlarge' size='large' %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="block">
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
|
<label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
|
||||||
{{ form.cover }}
|
{{ form.cover }}
|
||||||
</div>
|
</div>
|
||||||
{% if book %}
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_cover_url">
|
<label class="label" for="id_cover_url">
|
||||||
{% trans "Load cover from url:" %}
|
{% trans "Load cover from url:" %}
|
||||||
</label>
|
</label>
|
||||||
<input class="input" name="cover-url" id="id_cover_url">
|
<input class="input" name="cover-url" id="id_cover_url" type="url" value="{{ cover_url|default:'' }}">
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
{% for error in form.cover.errors %}
|
{% for error in form.cover.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="block">
|
<section class="block">
|
||||||
<h2 class="title is-4">{% trans "Physical Properties" %}</h2>
|
<h2 class="title is-4">{% trans "Physical Properties" %}</h2>
|
||||||
|
<div class="box">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-one-third">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_physical_format">{% trans "Format:" %}</label>
|
<label class="label" for="id_physical_format">{% trans "Format:" %}</label>
|
||||||
|
<div class="select">
|
||||||
{{ form.physical_format }}
|
{{ form.physical_format }}
|
||||||
|
</div>
|
||||||
{% for error in form.physical_format.errors %}
|
{% for error in form.physical_format.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="id_physical_format_detail">{% trans "Format details:" %}</label>
|
||||||
|
{{ form.physical_format_detail }}
|
||||||
|
{% for error in form.physical_format_detail.errors %}
|
||||||
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_pages">{% trans "Pages:" %}</label>
|
<label class="label" for="id_pages">{% trans "Pages:" %}</label>
|
||||||
|
@ -269,9 +191,11 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="block">
|
<section class="block">
|
||||||
<h2 class="title is-4">{% trans "Book Identifiers" %}</h2>
|
<h2 class="title is-4">{% trans "Book Identifiers" %}</h2>
|
||||||
|
<div class="box">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_isbn_13">{% trans "ISBN 13:" %}</label>
|
<label class="label" for="id_isbn_13">{% trans "ISBN 13:" %}</label>
|
||||||
{{ form.isbn_13 }}
|
{{ form.isbn_13 }}
|
||||||
|
@ -320,15 +244,6 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if not confirm_mode %}
|
|
||||||
<div class="block">
|
|
||||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
|
||||||
<a class="button" href="{{ book.local_path }}">{% trans "Cancel" %}</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,7 +0,0 @@
|
||||||
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
|
||||||
|
|
||||||
{% block filter_fields %}
|
|
||||||
{% include 'book/search_filter.html' %}
|
|
||||||
{% include 'book/language_filter.html' %}
|
|
||||||
{% include 'book/format_filter.html' %}
|
|
||||||
{% endblock %}
|
|
7
bookwyrm/templates/book/editions/edition_filters.html
Normal file
7
bookwyrm/templates/book/editions/edition_filters.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
||||||
|
|
||||||
|
{% block filter_fields %}
|
||||||
|
{% include 'book/editions/search_filter.html' %}
|
||||||
|
{% include 'book/editions/language_filter.html' %}
|
||||||
|
{% include 'book/editions/format_filter.html' %}
|
||||||
|
{% endblock %}
|
|
@ -8,7 +8,7 @@
|
||||||
<h1 class="title">{% blocktrans with work_path=work.local_path work_title=work|book_title %}Editions of <a href="{{ work_path }}">"{{ work_title }}"</a>{% endblocktrans %}</h1>
|
<h1 class="title">{% blocktrans with work_path=work.local_path work_title=work|book_title %}Editions of <a href="{{ work_path }}">"{{ work_title }}"</a>{% endblocktrans %}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'book/edition_filters.html' %}
|
{% include 'book/editions/edition_filters.html' %}
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{% for book in editions %}
|
{% for book in editions %}
|
|
@ -3,20 +3,20 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
|
{% firstof book.physical_format_detail book.physical_format as format %}
|
||||||
|
{% firstof book.physical_format book.physical_format_detail as format_property %}
|
||||||
|
{% with pages=book.pages %}
|
||||||
|
{% if format or pages %}
|
||||||
|
|
||||||
|
{% if format_property %}
|
||||||
|
<meta itemprop="bookFormat" content="{{ format_property }}">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if pages %}
|
||||||
|
<meta itemprop="numberOfPages" content="{{ pages }}">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{% with format=book.physical_format pages=book.pages %}
|
|
||||||
{% if format %}
|
|
||||||
{% comment %}
|
|
||||||
@todo The bookFormat property is limited to a list of values whereas the book edition is free text.
|
|
||||||
@see https://schema.org/bookFormat
|
|
||||||
{% endcomment %}
|
|
||||||
<meta itemprop="bookFormat" content="{{ format }}">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if pages %}
|
|
||||||
<meta itemprop="numberOfPages" content="{{ pages }}">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if format and not pages %}
|
{% if format and not pages %}
|
||||||
{% blocktrans %}{{ format }}{% endblocktrans %}
|
{% blocktrans %}{{ format }}{% endblocktrans %}
|
||||||
{% elif format and pages %}
|
{% elif format and pages %}
|
||||||
|
@ -24,8 +24,9 @@
|
||||||
{% elif pages %}
|
{% elif pages %}
|
||||||
{% blocktrans %}{{ pages }} pages{% endblocktrans %}
|
{% blocktrans %}{{ pages }} pages{% endblocktrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
|
||||||
</p>
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% if book.languages %}
|
{% if book.languages %}
|
||||||
{% for language in book.languages %}
|
{% for language in book.languages %}
|
||||||
|
@ -39,14 +40,15 @@
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p>
|
{% with date=book.published_date|naturalday publisher=book.publishers|join:', ' %}
|
||||||
{% with date=book.published_date|naturalday publisher=book.publishers|join:', ' %}
|
{% if date or book.first_published_date or book.publishers %}
|
||||||
{% if date or book.first_published_date %}
|
{% if date or book.first_published_date %}
|
||||||
<meta
|
<meta
|
||||||
itemprop="datePublished"
|
itemprop="datePublished"
|
||||||
content="{{ book.first_published_date|default:book.published_date|date:'Y-m-d' }}"
|
content="{{ book.first_published_date|default:book.published_date|date:'Y-m-d' }}"
|
||||||
>
|
>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<p>
|
||||||
|
|
||||||
{% comment %}
|
{% comment %}
|
||||||
@todo The publisher property needs to be an Organization or a Person. We’ll be using Thing which is the more generic ancestor.
|
@todo The publisher property needs to be an Organization or a Person. We’ll be using Thing which is the more generic ancestor.
|
||||||
|
@ -65,6 +67,7 @@
|
||||||
{% elif publisher %}
|
{% elif publisher %}
|
||||||
{% blocktrans %}Published by {{ publisher }}.{% endblocktrans %}
|
{% blocktrans %}Published by {{ publisher }}.{% endblocktrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
|
||||||
</p>
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
{% endspaceless %}
|
{% endspaceless %}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<section class="card is-hidden {{ class }}" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}">
|
<section class="card {% if not visible %}is-hidden {% endif %}{{ class }}" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}">
|
||||||
<header class="card-header has-background-white-ter">
|
<header class="card-header has-background-white-ter">
|
||||||
<h2 class="card-header-title" tabindex="0" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}_header">
|
<h2 class="card-header-title" tabindex="0" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}_header">
|
||||||
{% block header %}{% endblock %}
|
{% block header %}{% endblock %}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
{% trans "Local users" %}
|
{% trans "Local users" %}
|
||||||
</label>
|
</label>
|
||||||
<label class="is-block">
|
<label class="is-block">
|
||||||
<input type="radio" class="radio" name="scope" value="federated" {% if not request.GET.sort or request.GET.scope == "federated" %}checked{% endif %}>
|
<input type="radio" class="radio" name="scope" value="federated" {% if request.GET.scope == "federated" %}checked{% endif %}>
|
||||||
{% trans "Federated community" %}
|
{% trans "Federated community" %}
|
||||||
</label>
|
</label>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
<label class="label" for="id_sort">{% trans "Order by" %}</label>
|
<label class="label" for="id_sort">{% trans "Order by" %}</label>
|
||||||
<div class="select">
|
<div class="select">
|
||||||
<select name="sort" id="id_sort">
|
<select name="sort" id="id_sort">
|
||||||
<option value="suggested" {% if not request.GET.sort or request.GET.sort == "suggested" %}checked{% endif %}>{% trans "Suggested" %}</option>
|
<option value="recent" {% if request.GET.sort == "recent" %}selected{% endif %}>{% trans "Recently active" %}</option>
|
||||||
<option value="recent" {% if request.GET.sort == "suggested" %}checked{% endif %}>{% trans "Recently active" %}</option>
|
<option value="suggested" {% if request.GET.sort == "suggested" %}selected{% endif %}>{% trans "Suggested" %}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
{% if request.user.show_goal and not goal and tab.key == 'home' %}
|
{% if request.user.show_goal and not goal and tab.key == 'home' %}
|
||||||
{% now 'Y' as year %}
|
{% now 'Y' as year %}
|
||||||
<section class="block">
|
<section class="block">
|
||||||
{% include 'snippets/goal_card.html' with year=year %}
|
{% include 'feed/goal_card.html' with year=year %}
|
||||||
<hr>
|
<hr>
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -7,13 +7,8 @@
|
||||||
</h3>
|
</h3>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block card-content %}
|
{% block card-content %}
|
||||||
<div class="content">
|
{% include 'snippets/goal_form.html' %}
|
||||||
<p>{% blocktrans %}Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.{% endblocktrans %}</p>
|
|
||||||
|
|
||||||
{% include 'snippets/goal_form.html' %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block card-footer %}
|
{% block card-footer %}
|
|
@ -1,6 +1,7 @@
|
||||||
{% load status_display %}
|
{% load status_display %}
|
||||||
<div class="block">
|
|
||||||
|
|
||||||
|
<div class="thread-parent is-relative block">
|
||||||
|
<div class="thread">
|
||||||
{% with depth=depth|add:1 %}
|
{% with depth=depth|add:1 %}
|
||||||
{% if depth <= max_depth and status.reply_parent and direction <= 0 %}
|
{% if depth <= max_depth and status.reply_parent and direction <= 0 %}
|
||||||
{% with direction=-1 %}
|
{% with direction=-1 %}
|
||||||
|
@ -8,7 +9,9 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% include 'snippets/status/status.html' with status=status main=is_root %}
|
<div{% if is_root %} class="block mt-5 is-main"{% endif %}>
|
||||||
|
{% include 'snippets/status/status.html' with status=status main=is_root %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if depth <= max_depth and direction >= 0 %}
|
{% if depth <= max_depth and direction >= 0 %}
|
||||||
{% for reply in status|replies %}
|
{% for reply in status|replies %}
|
||||||
|
@ -18,5 +21,5 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
<link rel="stylesheet" href="{% static "css/vendor/icons.css" %}">
|
<link rel="stylesheet" href="{% static "css/vendor/icons.css" %}">
|
||||||
<link rel="stylesheet" href="{% static "css/bookwyrm.css" %}">
|
<link rel="stylesheet" href="{% static "css/bookwyrm.css" %}">
|
||||||
|
|
||||||
|
<link rel="search" type="application/opensearchdescription+xml" href="{% url 'opensearch' %}" title="{% blocktrans with site_name=site.name %}{{ site_name }} search{% endblocktrans %}" />
|
||||||
|
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{% get_media_prefix %}{{ site.favicon }}{% else %}{% static "images/favicon.ico" %}{% endif %}">
|
<link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{% get_media_prefix %}{{ site.favicon }}{% else %}{% static "images/favicon.ico" %}{% endif %}">
|
||||||
|
|
||||||
{% if preview_images_enabled is True %}
|
{% if preview_images_enabled is True %}
|
||||||
|
@ -34,7 +36,7 @@
|
||||||
<a class="navbar-item" href="/">
|
<a class="navbar-item" href="/">
|
||||||
<img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="Home page">
|
<img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="Home page">
|
||||||
</a>
|
</a>
|
||||||
<form class="navbar-item column" action="/search/">
|
<form class="navbar-item column" action="{% url 'search' %}">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
|
@ -115,7 +117,7 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if perms.bookwyrm.create_invites or perms.moderate_user %}
|
{% if perms.bookwyrm.create_invites or perms.moderate_user %}
|
||||||
<li class="navbar-divider" role="presentation"></li>
|
<li class="navbar-divider" role="presentation"> </li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.bookwyrm.create_invites and not site.allow_registration %}
|
{% if perms.bookwyrm.create_invites and not site.allow_registration %}
|
||||||
<li>
|
<li>
|
||||||
|
@ -131,7 +133,7 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="navbar-divider" role="presentation"></li>
|
<li class="navbar-divider" role="presentation"> </li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'logout' %}" class="navbar-item">
|
<a href="{% url 'logout' %}" class="navbar-item">
|
||||||
{% trans 'Log out' %}
|
{% trans 'Log out' %}
|
||||||
|
|
|
@ -67,14 +67,14 @@
|
||||||
<p>{% blocktrans with username=item.user.display_name user_path=item.user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p>
|
<p>{% blocktrans with username=item.user.display_name user_path=item.user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if list.user == request.user or list.curation == 'open' and item.user == request.user or list.group|is_member:request.user %}
|
{% if list.user == request.user or list.group|is_member:request.user %}
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
<form name="set-position" method="post" action="{% url 'list-set-book-position' item.id %}">
|
<form name="set-position" method="post" action="{% url 'list-set-book-position' item.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
<div class="field has-addons mb-0">
|
<div class="field has-addons mb-0">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label for="input-list-position" class="button is-transparent is-small">{% trans "List position" %}</label>
|
<label for="input-list-position" class="button is-transparent is-small">{% trans "List position" %}</label>
|
||||||
</div>
|
</div>
|
||||||
{% csrf_token %}
|
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input id="input_list_position" class="input is-small" type="number" min="1" name="position" value="{{ item.order }}">
|
<input id="input_list_position" class="input is-small" type="number" min="1" name="position" value="{{ item.order }}">
|
||||||
</div>
|
</div>
|
||||||
|
@ -84,7 +84,9 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<form name="add-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item">
|
{% endif %}
|
||||||
|
{% if list.user == request.user or list.curation == 'open' and item.user == request.user %}
|
||||||
|
<form name="remove-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="item" value="{{ item.id }}">
|
<input type="hidden" name="item" value="{{ item.id }}">
|
||||||
<button type="submit" class="button is-small is-danger">{% trans "Remove" %}</button>
|
<button type="submit" class="button is-small is-danger">{% trans "Remove" %}</button>
|
||||||
|
|
|
@ -1,187 +0,0 @@
|
||||||
{% extends 'layout.html' %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
{% load humanize %}
|
|
||||||
|
|
||||||
{% block title %}{% trans "Notifications" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<header class="columns">
|
|
||||||
<div class="column">
|
|
||||||
<h1 class="title">{% trans "Notifications" %}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form name="clear" action="/notifications" method="POST" class="column is-narrow">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button class="button is-danger is-light" type="submit" class="secondary">{% trans "Delete notifications" %}</button>
|
|
||||||
</form>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="block">
|
|
||||||
<nav class="tabs">
|
|
||||||
<ul>
|
|
||||||
{% url 'notifications' as tab_url %}
|
|
||||||
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
|
||||||
<a href="{{ tab_url }}">{% trans "All" %}</a>
|
|
||||||
</li>
|
|
||||||
{% url 'notifications' 'mentions' as tab_url %}
|
|
||||||
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
|
||||||
<a href="{{ tab_url }}">{% trans "Mentions" %}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="block">
|
|
||||||
{% for notification in notifications %}
|
|
||||||
{% related_status notification as related_status %}
|
|
||||||
<div class="notification {% if notification.id in unread %} is-primary{% endif %}">
|
|
||||||
<div class="columns is-mobile">
|
|
||||||
<div class="column is-narrow is-size-3 {% if notification.id in unread%}has-text-white{% else %}has-text-grey{% endif %}">
|
|
||||||
{% if notification.notification_type == 'MENTION' %}
|
|
||||||
<span class="icon icon-comment"></span>
|
|
||||||
{% elif notification.notification_type == 'REPLY' %}
|
|
||||||
<span class="icon icon-comments"></span>
|
|
||||||
{% elif notification.notification_type == 'FOLLOW' or notification.notification_type == 'FOLLOW_REQUEST' or notification.notification_type == 'INVITE' or notification.notification_type == 'ACCEPT' or notification.notification_type == 'JOIN' or notification.notification_type == 'LEAVE' or notification.notification_type == 'REMOVE'%}
|
|
||||||
<span class="icon icon-local"></span>
|
|
||||||
{% elif notification.notification_type == 'BOOST' %}
|
|
||||||
<span class="icon icon-boost"></span>
|
|
||||||
{% elif notification.notification_type == 'FAVORITE' %}
|
|
||||||
<span class="icon icon-heart"></span>
|
|
||||||
{% elif notification.notification_type == 'IMPORT' %}
|
|
||||||
<span class="icon icon-list"></span>
|
|
||||||
{% elif notification.notification_type == 'ADD' %}
|
|
||||||
<span class="icon icon-plus"></span>
|
|
||||||
{% elif notification.notification_type == 'REPORT' %}
|
|
||||||
<span class="icon icon-warning"></span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="column is-clipped">
|
|
||||||
<div class="block">
|
|
||||||
<p>
|
|
||||||
{# DESCRIPTION #}
|
|
||||||
{% if notification.related_user %}
|
|
||||||
<a href="{{ notification.related_user.local_path }}">
|
|
||||||
{% include 'snippets/avatar.html' with user=notification.related_user %}
|
|
||||||
{{ notification.related_user.display_name }}
|
|
||||||
</a>
|
|
||||||
{% if notification.notification_type == 'FAVORITE' %}
|
|
||||||
{% if related_status.status_type == 'Review' %}
|
|
||||||
{% blocktrans with book_title=related_status.book.title related_path=related_status.local_path %}favorited your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>{% endblocktrans %}
|
|
||||||
{% elif related_status.status_type == 'Comment' %}
|
|
||||||
{% blocktrans with book_title=related_status.book.title related_path=related_status.local_path %}favorited your <a href="{{ related_path }}">comment on <em>{{ book_title }}</em></a>{% endblocktrans %}
|
|
||||||
{% elif related_status.status_type == 'Quotation' %}
|
|
||||||
{% blocktrans with book_title=related_status.book.title related_path=related_status.local_path %}favorited your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>{% endblocktrans %}
|
|
||||||
{% else %}
|
|
||||||
{% blocktrans with related_path=related_status.local_path %}favorited your <a href="{{ related_path }}">status</a>{% endblocktrans %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% elif notification.notification_type == 'MENTION' %}
|
|
||||||
{% if related_status.status_type == 'Review' %}
|
|
||||||
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}mentioned you in a <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>{% endblocktrans %}
|
|
||||||
{% elif related_status.status_type == 'Comment' %}
|
|
||||||
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}mentioned you in a <a href="{{ related_path }}">comment on <em>{{ book_title }}</em></a>{% endblocktrans %}
|
|
||||||
{% elif related_status.status_type == 'Quotation' %}
|
|
||||||
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}mentioned you in a <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>{% endblocktrans %}
|
|
||||||
{% else %}
|
|
||||||
{% blocktrans with related_path=related_status.local_path %}mentioned you in a <a href="{{ related_path }}">status</a>{% endblocktrans %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% elif notification.notification_type == 'REPLY' %}
|
|
||||||
{% if related_status.status_type == 'Review' %}
|
|
||||||
{% blocktrans with related_path=related_status.local_path parent_path=related_status.reply_parent.local_path book_title=related_status.reply_parent.book.title %}<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">review of <em>{{ book_title }}</em></a>{% endblocktrans %}
|
|
||||||
{% elif related_status.status_type == 'Comment' %}
|
|
||||||
{% blocktrans with related_path=related_status.local_path parent_path=related_status.reply_parent.local_path book_title=related_status.reply_parent.book.title %}<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">comment on <em>{{ book_title }}</em></a>{% endblocktrans %}
|
|
||||||
{% elif related_status.status_type == 'Quotation' %}
|
|
||||||
{% blocktrans with related_path=related_status.local_path parent_path=related_status.reply_parent.local_path book_title=related_status.reply_parent.book.title %}<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">quote from <em>{{ book_title }}</em></a>{% endblocktrans %}
|
|
||||||
{% else %}
|
|
||||||
{% blocktrans with related_path=related_status.local_path parent_path=related_status.reply_parent.local_path %}<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">status</a>{% endblocktrans %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% elif notification.notification_type == 'FOLLOW' %}
|
|
||||||
{% trans "followed you" %}
|
|
||||||
{% include 'snippets/follow_button.html' with user=notification.related_user %}
|
|
||||||
{% elif notification.notification_type == 'FOLLOW_REQUEST' %}
|
|
||||||
{% trans "sent you a follow request" %}
|
|
||||||
<div class="row shrink">
|
|
||||||
{% include 'snippets/follow_request_buttons.html' with user=notification.related_user %}
|
|
||||||
</div>
|
|
||||||
{% elif notification.notification_type == 'BOOST' %}
|
|
||||||
{% if related_status.status_type == 'Review' %}
|
|
||||||
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}boosted your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>{% endblocktrans %}
|
|
||||||
{% elif related_status.status_type == 'Comment' %}
|
|
||||||
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}boosted your <a href="{{ related_path }}">comment on<em>{{ book_title }}</em></a>{% endblocktrans %}
|
|
||||||
{% elif related_status.status_type == 'Quotation' %}
|
|
||||||
{% blocktrans with related_path=related_status.local_path book_title=related_status.book.title %}boosted your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>{% endblocktrans %}
|
|
||||||
{% else %}
|
|
||||||
{% blocktrans with related_path=related_status.local_path %}boosted your <a href="{{ related_path }}">status</a>{% endblocktrans %}
|
|
||||||
{% endif %}
|
|
||||||
{% elif notification.notification_type == 'ADD' %}
|
|
||||||
{% if notification.related_list_item.approved %}
|
|
||||||
{% blocktrans with book_path=notification.related_list_item.book.local_path book_title=notification.related_list_item.book.title list_path=notification.related_list_item.book_list.local_path list_name=notification.related_list_item.book_list.name %} added <em><a href="{{ book_path }}">{{ book_title }}</a></em> to your list "<a href="{{ list_path }}">{{ list_name }}</a>"{% endblocktrans %}
|
|
||||||
{% else %}
|
|
||||||
{% blocktrans with book_path=notification.related_list_item.book.local_path book_title=notification.related_list_item.book.title list_path=notification.related_list_item.book_list.local_path list_name=notification.related_list_item.book_list.name %} suggested adding <em><a href="{{ book_path }}">{{ book_title }}</a></em> to your list "<a href="{{ list_path }}/curate">{{ list_name }}</a>"{% endblocktrans %}
|
|
||||||
{% endif %}
|
|
||||||
{% elif notification.notification_type == 'INVITE' %}
|
|
||||||
{% if notification.related_group %}
|
|
||||||
{% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} invited you to join the group <a href="{{ group_path }}">{{ group_name }}</a> {% endblocktrans %}
|
|
||||||
<div class="row shrink">
|
|
||||||
{% include 'snippets/join_invitation_buttons.html' with group=notification.related_group %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% elif notification.notification_type == 'ACCEPT' %}
|
|
||||||
{% if notification.related_group %}
|
|
||||||
{% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} accepted your invitation to join group "<a href="{{ group_path }}">{{ group_name }}</a>"{% endblocktrans %}
|
|
||||||
{% endif %}
|
|
||||||
{% elif notification.notification_type == 'JOIN' %}
|
|
||||||
{% if notification.related_group %}
|
|
||||||
{% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} has joined your group "<a href="{{ group_path }}">{{ group_name }}</a>"{% endblocktrans %}
|
|
||||||
{% endif %}
|
|
||||||
{% elif notification.notification_type == 'LEAVE' %}
|
|
||||||
{% if notification.related_group %}
|
|
||||||
{% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} has left your group "<a href="{{ group_path }}">{{ group_name }}</a>"{% endblocktrans %}
|
|
||||||
{% endif %}
|
|
||||||
{% elif notification.notification_type == 'REMOVE' %}
|
|
||||||
{% if notification.related_group %}
|
|
||||||
{% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %} has been removed from your group "<a href="{{ group_path }}">{{ group_name }}</a>"{% endblocktrans %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% elif notification.notification_type == 'REMOVE' and notification.related_group %}
|
|
||||||
{% blocktrans with group_name=notification.related_group.name group_path=notification.related_group.local_path %}
|
|
||||||
You have been removed from the "<a href="{{ group_path }}">{{ group_name }}</a> group"
|
|
||||||
{% endblocktrans %}
|
|
||||||
{% elif notification.related_import %}
|
|
||||||
{% 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 %}
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{% if related_status %}
|
|
||||||
<div class="block">
|
|
||||||
{# PREVIEW #}
|
|
||||||
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white{% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %} has-text-black{% else %}-bis has-text-grey-dark{% endif %}{% endif %}">
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column is-clipped">
|
|
||||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
|
||||||
</div>
|
|
||||||
<div class="column is-narrow {% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %}has-text-black{% else %}has-text-grey-dark{% endif %}">
|
|
||||||
{{ related_status.published_date|timesince }}
|
|
||||||
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if not notifications %}
|
|
||||||
<p>{% trans "You're all caught up!" %}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
20
bookwyrm/templates/notifications/item.html
Normal file
20
bookwyrm/templates/notifications/item.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{# load the right template #}
|
||||||
|
{% if notification.notification_type == 'MENTION' %}
|
||||||
|
{% include 'notifications/items/mention.html' %}
|
||||||
|
{% elif notification.notification_type == 'REPLY' %}
|
||||||
|
{% include 'notifications/items/reply.html' %}
|
||||||
|
{% elif notification.notification_type == 'BOOST' %}
|
||||||
|
{% include 'notifications/items/boost.html' %}
|
||||||
|
{% elif notification.notification_type == 'FAVORITE' %}
|
||||||
|
{% include 'notifications/items/fav.html' %}
|
||||||
|
{% elif notification.notification_type == 'FOLLOW' %}
|
||||||
|
{% include 'notifications/items/follow.html' %}
|
||||||
|
{% elif notification.notification_type == 'FOLLOW_REQUEST' %}
|
||||||
|
{% include 'notifications/items/follow_request.html' %}
|
||||||
|
{% elif notification.notification_type == 'IMPORT' %}
|
||||||
|
{% include 'notifications/items/import.html' %}
|
||||||
|
{% elif notification.notification_type == 'ADD' %}
|
||||||
|
{% include 'notifications/items/add.html' %}
|
||||||
|
{% elif notification.notification_type == 'REPORT' %}
|
||||||
|
{% include 'notifications/items/report.html' %}
|
||||||
|
{% endif %}
|
42
bookwyrm/templates/notifications/items/add.html
Normal file
42
bookwyrm/templates/notifications/items/add.html
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{% extends 'notifications/items/item_layout.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
|
||||||
|
{% block primary_link %}{% spaceless %}
|
||||||
|
{% if notification.related_list_item.approved %}
|
||||||
|
{{ notification.related_list_item.book_list.local_path }}
|
||||||
|
{% else %}
|
||||||
|
{% url 'list-curate' notification.related_list_item.book_list.id %}
|
||||||
|
{% endif %}
|
||||||
|
{% endspaceless %}{% endblock %}
|
||||||
|
|
||||||
|
{% block icon %}
|
||||||
|
<span class="icon icon-plus"></span>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block description %}
|
||||||
|
{% with book_path=notification.related_list_item.book.local_path %}
|
||||||
|
{% with book_title=notification.related_list_item.book|book_title %}
|
||||||
|
{% with list_name=notification.related_list_item.book_list.name %}
|
||||||
|
|
||||||
|
{% if notification.related_list_item.approved %}
|
||||||
|
{% blocktrans trimmed with list_path=notification.related_list_item.book_list.local_path %}
|
||||||
|
|
||||||
|
added <em><a href="{{ book_path }}">{{ book_title }}</a></em> to your list "<a href="{{ list_path }}">{{ list_name }}</a>"
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% url 'list-curate' notification.related_list_item.book_list.id as list_path %}
|
||||||
|
{% blocktrans trimmed with list_path=list_path %}
|
||||||
|
|
||||||
|
suggested adding <em><a href="{{ book_path }}">{{ book_title }}</a></em> to your list "<a href="{{ list_path }}">{{ list_name }}</a>"
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
||||||
|
|
61
bookwyrm/templates/notifications/items/boost.html
Normal file
61
bookwyrm/templates/notifications/items/boost.html
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{% extends 'notifications/items/item_layout.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
|
||||||
|
{% block primary_link %}{% spaceless %}
|
||||||
|
{{ notification.related_status.local_path }}
|
||||||
|
{% endspaceless %}{% endblock %}
|
||||||
|
|
||||||
|
{% block icon %}
|
||||||
|
<span class="icon icon-boost"></span>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block description %}
|
||||||
|
{% with related_status.book|book_title as book_title %}
|
||||||
|
{% with related_status.local_path as related_path %}
|
||||||
|
|
||||||
|
{% if related_status.status_type == 'Review' %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
boosted your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% elif related_status.status_type == 'Comment' %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
boosted your <a href="{{ related_path }}">comment on<em>{{ book_title }}</em></a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% elif related_status.status_type == 'Quotation' %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
boosted your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
boosted your <a href="{{ related_path }}">status</a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block preview %}
|
||||||
|
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white has-text-grey-dark{% endif %}">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-clipped">
|
||||||
|
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow has-grey-dark">
|
||||||
|
{{ related_status.published_date|timesince }}
|
||||||
|
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
61
bookwyrm/templates/notifications/items/fav.html
Normal file
61
bookwyrm/templates/notifications/items/fav.html
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{% extends 'notifications/items/item_layout.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
|
||||||
|
{% block primary_link %}{% spaceless %}
|
||||||
|
{{ notification.related_status.local_path }}
|
||||||
|
{% endspaceless %}{% endblock %}
|
||||||
|
|
||||||
|
{% block icon %}
|
||||||
|
<span class="icon icon-heart"></span>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block description %}
|
||||||
|
{% with related_status.book|book_title as book_title %}
|
||||||
|
{% with related_status.local_path as related_path %}
|
||||||
|
|
||||||
|
{% if related_status.status_type == 'Review' %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
favorited your <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% elif related_status.status_type == 'Comment' %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
favorited your <a href="{{ related_path }}">comment on<em>{{ book_title }}</em></a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% elif related_status.status_type == 'Quotation' %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
favorited your <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
favorited your <a href="{{ related_path }}">status</a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block preview %}
|
||||||
|
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white has-text-grey-dark{% endif %}">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-clipped">
|
||||||
|
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow has-grey-dark">
|
||||||
|
{{ related_status.published_date|timesince }}
|
||||||
|
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
17
bookwyrm/templates/notifications/items/follow.html
Normal file
17
bookwyrm/templates/notifications/items/follow.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends 'notifications/items/item_layout.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
|
||||||
|
{% block primary_link %}{% spaceless %}
|
||||||
|
{{ notification.related_user.local_path }}
|
||||||
|
{% endspaceless %}{% endblock %}
|
||||||
|
|
||||||
|
{% block icon %}
|
||||||
|
<span class="icon icon-local"></span>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block description %}
|
||||||
|
{% trans "followed you" %}
|
||||||
|
{% include 'snippets/follow_button.html' with user=notification.related_user %}
|
||||||
|
{% endblock %}
|
15
bookwyrm/templates/notifications/items/follow_request.html
Normal file
15
bookwyrm/templates/notifications/items/follow_request.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends 'notifications/items/item_layout.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
|
||||||
|
{% block icon %}
|
||||||
|
<span class="icon icon-local"></span>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block description %}
|
||||||
|
{% trans "sent you a follow request" %}
|
||||||
|
<div class="row shrink">
|
||||||
|
{% include 'snippets/follow_request_buttons.html' with user=notification.related_user %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
15
bookwyrm/templates/notifications/items/import.html
Normal file
15
bookwyrm/templates/notifications/items/import.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends 'notifications/items/item_layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block primary_link %}{% spaceless %}
|
||||||
|
{% url 'import-status' notification.related_import.id %}
|
||||||
|
{% endspaceless %}{% endblock %}
|
||||||
|
|
||||||
|
{% block icon %}
|
||||||
|
<span class="icon icon-list"></span>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block description %}
|
||||||
|
{% url 'import-status' notification.related_import.id as url %}
|
||||||
|
{% blocktrans %}Your <a href="{{ url }}">import</a> completed.{% endblocktrans %}
|
||||||
|
{% endblock %}
|
29
bookwyrm/templates/notifications/items/item_layout.html
Normal file
29
bookwyrm/templates/notifications/items/item_layout.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{% load humanize %}
|
||||||
|
{% load bookwyrm_tags %}
|
||||||
|
{% related_status notification as related_status %}
|
||||||
|
<div class="notification is-clickable {% if notification.id in unread %} is-primary{% endif %}" data-href="{% block primary_link %}{% endblock %}">
|
||||||
|
<div class="columns is-mobile">
|
||||||
|
<div class="column is-narrow is-size-3 {% if notification.id in unread%}has-text-white{% else %}has-text-grey{% endif %}">
|
||||||
|
{% block icon %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
<div class="column is-clipped">
|
||||||
|
<div class="block">
|
||||||
|
<p>
|
||||||
|
{% if notification.related_user %}
|
||||||
|
<a href="{{ notification.related_user.local_path }}">
|
||||||
|
{% include 'snippets/avatar.html' with user=notification.related_user %}
|
||||||
|
{{ notification.related_user.display_name }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% block description %}{% endblock %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% if related_status %}
|
||||||
|
<div class="block">
|
||||||
|
{% block preview %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
62
bookwyrm/templates/notifications/items/mention.html
Normal file
62
bookwyrm/templates/notifications/items/mention.html
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
{% extends 'notifications/items/item_layout.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
|
||||||
|
{% block primary_link %}{% spaceless %}
|
||||||
|
{{ notification.related_status.local_path }}
|
||||||
|
{% endspaceless %}{% endblock %}
|
||||||
|
|
||||||
|
{% block icon %}
|
||||||
|
<span class="icon icon-comment"></span>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block description %}
|
||||||
|
{% with related_status.book|book_title as book_title %}
|
||||||
|
{% with related_status.local_path as related_path %}
|
||||||
|
|
||||||
|
{% if related_status.status_type == 'Review' %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
mentioned you in a <a href="{{ related_path }}">review of <em>{{ book_title }}</em></a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% elif related_status.status_type == 'Comment' %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
mentioned you in a <a href="{{ related_path }}">comment on <em>{{ book_title }}</em></a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% elif related_status.status_type == 'Quotation' %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
mentioned you in a <a href="{{ related_path }}">quote from <em>{{ book_title }}</em></a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
mentioned you in a <a href="{{ related_path }}">status</a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block preview %}
|
||||||
|
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white has-text-black{% endif %}">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-clipped">
|
||||||
|
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow has-text-black">
|
||||||
|
{{ related_status.published_date|timesince }}
|
||||||
|
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
65
bookwyrm/templates/notifications/items/reply.html
Normal file
65
bookwyrm/templates/notifications/items/reply.html
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
{% extends 'notifications/items/item_layout.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
|
||||||
|
{% block primary_link %}{% spaceless %}
|
||||||
|
{{ notification.related_status.local_path }}
|
||||||
|
{% endspaceless %}{% endblock %}
|
||||||
|
|
||||||
|
{% block icon %}
|
||||||
|
<span class="icon icon-comments"></span>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block description %}
|
||||||
|
{% with related_status.reply_parent.book|book_title as book_title %}
|
||||||
|
{% with related_status.local_path as related_path %}
|
||||||
|
{% with related_status.reply_parent.local_path as parent_path %}
|
||||||
|
|
||||||
|
{% if related_status.reply_parent.status_type == 'Review' %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">review of <em>{{ book_title }}</em></a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% elif related_status.reply_parent.status_type == 'Comment' %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">comment on <em>{{ book_title }}</em></a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% elif related_status.reply_parent.status_type == 'Quotation' %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">quote from <em>{{ book_title }}</em></a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
|
||||||
|
<a href="{{ related_path }}">replied</a> to your <a href="{{ parent_path }}">status</a>
|
||||||
|
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% block preview %}
|
||||||
|
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white has-text-black{% endif %}">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-clipped">
|
||||||
|
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow has-text-black">
|
||||||
|
{{ related_status.published_date|timesince }}
|
||||||
|
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
16
bookwyrm/templates/notifications/items/report.html
Normal file
16
bookwyrm/templates/notifications/items/report.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends 'notifications/items/item_layout.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block primary_link %}{% spaceless %}
|
||||||
|
{% url 'settings-report' notification.related_report.id %}
|
||||||
|
{% endspaceless %}{% endblock %}
|
||||||
|
|
||||||
|
{% block icon %}
|
||||||
|
<span class="icon icon-warning"></span>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block description %}
|
||||||
|
{% url 'settings-report' notification.related_report.id as path %}
|
||||||
|
{% blocktrans %}A new <a href="{{ path }}">report</a> needs moderation.{% endblocktrans %}
|
||||||
|
{% endblock %}
|
52
bookwyrm/templates/notifications/notifications_page.html
Normal file
52
bookwyrm/templates/notifications/notifications_page.html
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Notifications" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<header class="columns is-mobile">
|
||||||
|
<div class="column">
|
||||||
|
<h1 class="title">{% trans "Notifications" %}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form name="clear" action="/notifications" method="POST" class="column is-narrow">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% spaceless %}
|
||||||
|
<button class="button is-danger is-light" type="submit">
|
||||||
|
<span class="icon icon-x m-0-mobile" aria-hidden="true"></span>
|
||||||
|
<span class="is-sr-only-mobile">{% trans "Delete notifications" %}</span>
|
||||||
|
</button>
|
||||||
|
{% endspaceless %}
|
||||||
|
</form>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<nav class="tabs">
|
||||||
|
<ul>
|
||||||
|
{% url 'notifications' as tab_url %}
|
||||||
|
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||||
|
<a href="{{ tab_url }}">{% trans "All" %}</a>
|
||||||
|
</li>
|
||||||
|
{% url 'notifications' 'mentions' as tab_url %}
|
||||||
|
<li {% if tab_url == request.path %}class="is-active"{% endif %}>
|
||||||
|
<a href="{{ tab_url }}">{% trans "Mentions" %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
{% for notification in notifications %}
|
||||||
|
{% include 'notifications/item.html' %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if not notifications %}
|
||||||
|
<p>{% trans "You're all caught up!" %}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{% static "js/block_href.js" %}?v={{ js_cache }}"></script>
|
||||||
|
{% endblock %}
|
16
bookwyrm/templates/opensearch.xml
Normal file
16
bookwyrm/templates/opensearch.xml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{% load i18n %}{% load static %}<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<OpenSearchDescription
|
||||||
|
xmlns="http://a9.com/-/spec/opensearch/1.1/"
|
||||||
|
xmlns:moz="http://www.mozilla.org/2006/browser/search/"
|
||||||
|
>
|
||||||
|
<ShortName>BW</ShortName>
|
||||||
|
<Description>{% blocktrans trimmed with site_name=site.name %}
|
||||||
|
{{ site_name }} search
|
||||||
|
{% endblocktrans %}</Description>
|
||||||
|
<Image width="16" height="16" type="image/x-icon">{{ image }}</Image>
|
||||||
|
<Url
|
||||||
|
type="text/html"
|
||||||
|
method="get"
|
||||||
|
template="https://{{ DOMAIN }}{% url 'search' %}?q={searchTerms}"
|
||||||
|
/>
|
||||||
|
</OpenSearchDescription>
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
{% if not request.user.blocks.exists %}
|
{% if not request.user.blocks.exists %}
|
||||||
<p>{% trans "No users currently blocked." %}</p>
|
<p><em>{% trans "No users currently blocked." %}</em></p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for user in request.user.blocks.all %}
|
{% for user in request.user.blocks.all %}
|
||||||
|
|
|
@ -10,11 +10,11 @@
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
<form name="edit-profile" action="{% url 'prefs-password' %}" method="post" enctype="multipart/form-data">
|
<form name="edit-profile" action="{% url 'prefs-password' %}" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="block">
|
<div class="field">
|
||||||
<label class="label" for="id_password">{% trans "New password:" %}</label>
|
<label class="label" for="id_password">{% trans "New password:" %}</label>
|
||||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password">
|
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password">
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="field">
|
||||||
<label class="label" for="id_confirm_password">{% trans "Confirm password:" %}</label>
|
<label class="label" for="id_confirm_password">{% trans "Confirm password:" %}</label>
|
||||||
<input type="password" name="confirm-password" maxlength="128" class="input" required="" id="id_confirm_password">
|
<input type="password" name="confirm-password" maxlength="128" class="input" required="" id="id_confirm_password">
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,46 +7,72 @@
|
||||||
{% trans "Edit Profile" %}
|
{% trans "Edit Profile" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block profile-tabs %}
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li><a href="#profile">{% trans "Profile" %}</a></li>
|
||||||
|
<li><a href="#display-preferences">{% trans "Display preferences" %}</a></li>
|
||||||
|
<li><a href="#privacy">{% trans "Privacy" %}</a></li>
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
{% if form.non_field_errors %}
|
{% if form.non_field_errors %}
|
||||||
<p class="notification is-danger">{{ form.non_field_errors }}</p>
|
<p class="notification is-danger">{{ form.non_field_errors }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form name="edit-profile" action="{% url 'prefs-profile' %}" method="post" enctype="multipart/form-data">
|
<form name="edit-profile" action="{% url 'prefs-profile' %}" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="block">
|
<section class="block" id="profile">
|
||||||
|
<h2 class="title is-4">{% trans "Profile" %}</h2>
|
||||||
|
<div class="box">
|
||||||
<label class="label" for="id_avatar">{% trans "Avatar:" %}</label>
|
<label class="label" for="id_avatar">{% trans "Avatar:" %}</label>
|
||||||
|
<div class="field columns is-mobile">
|
||||||
|
{% if request.user.avatar %}
|
||||||
|
<div class="column is-narrow">
|
||||||
|
{% include 'snippets/avatar.html' with user=request.user large=True %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="column">
|
||||||
{{ form.avatar }}
|
{{ form.avatar }}
|
||||||
{% for error in form.avatar.errors %}
|
{% for error in form.avatar.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
</div>
|
||||||
|
<div class="field">
|
||||||
<label class="label" for="id_name">{% trans "Display name:" %}</label>
|
<label class="label" for="id_name">{% trans "Display name:" %}</label>
|
||||||
{{ form.name }}
|
{{ form.name }}
|
||||||
{% for error in form.name.errors %}
|
{% for error in form.name.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="field">
|
||||||
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
|
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
|
||||||
{{ form.summary }}
|
{{ form.summary }}
|
||||||
{% for error in form.summary.errors %}
|
{% for error in form.summary.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="field">
|
||||||
<label class="label" for="id_email">{% trans "Email address:" %}</label>
|
<label class="label" for="id_email">{% trans "Email address:" %}</label>
|
||||||
{{ form.email }}
|
{{ form.email }}
|
||||||
{% for error in form.email.errors %}
|
{% for error in form.email.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr aria-hidden="true">
|
||||||
|
|
||||||
|
<section class="block" id="display-preferences">
|
||||||
|
<h2 class="title is-4">{% trans "Display preferences" %}</h2>
|
||||||
|
<div class="box">
|
||||||
|
<div class="field">
|
||||||
<label class="checkbox label" for="id_show_goal">
|
<label class="checkbox label" for="id_show_goal">
|
||||||
{% trans "Show reading goal prompt in feed:" %}
|
{% trans "Show reading goal prompt in feed:" %}
|
||||||
{{ form.show_goal }}
|
{{ form.show_goal }}
|
||||||
</label>
|
</label>
|
||||||
<label class="checkbox label" for="id_show_goal">
|
<label class="checkbox label" for="id_show_suggested_users">
|
||||||
{% trans "Show suggested users:" %}
|
{% trans "Show suggested users:" %}
|
||||||
{{ form.show_suggested_users }}
|
{{ form.show_suggested_users }}
|
||||||
</label>
|
</label>
|
||||||
|
@ -55,15 +81,31 @@
|
||||||
{{ form.discoverable }}
|
{{ form.discoverable }}
|
||||||
</label>
|
</label>
|
||||||
{% url 'directory' as path %}
|
{% url 'directory' as path %}
|
||||||
<p class="help">{% blocktrans %}Your account will show up in the <a href="{{ path }}">directory</a>, and may be recommended to other BookWyrm users.{% endblocktrans %}</p>
|
<p class="help">
|
||||||
|
{% blocktrans %}Your account will show up in the <a href="{{ path }}">directory</a>, and may be recommended to other BookWyrm users.{% endblocktrans %}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="field">
|
||||||
|
<label class="label" for="id_preferred_timezone">{% trans "Preferred Timezone: " %}</label>
|
||||||
|
<div class="select">
|
||||||
|
{{ form.preferred_timezone }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr aria-hidden="true">
|
||||||
|
|
||||||
|
<section class="block" id="privacy">
|
||||||
|
<h2 class="title is-4">{% trans "Privacy" %}</h2>
|
||||||
|
<div class="box">
|
||||||
|
<div class="field">
|
||||||
<label class="checkbox label" for="id_manually_approves_followers">
|
<label class="checkbox label" for="id_manually_approves_followers">
|
||||||
{% trans "Manually approve followers:" %}
|
{% trans "Manually approve followers:" %}
|
||||||
{{ form.manually_approves_followers }}
|
{{ form.manually_approves_followers }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="field">
|
||||||
<label class="label" for="id_default_post_privacy">
|
<label class="label" for="id_default_post_privacy">
|
||||||
{% trans "Default post privacy:" %}
|
{% trans "Default post privacy:" %}
|
||||||
</label>
|
</label>
|
||||||
|
@ -71,12 +113,8 @@
|
||||||
{{ form.default_post_privacy }}
|
{{ form.default_post_privacy }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
|
||||||
<label class="label" for="id_preferred_timezone">{% trans "Preferred Timezone: " %}</label>
|
|
||||||
<div class="select">
|
|
||||||
{{ form.preferred_timezone }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
<div class="block"><button class="button is-primary" type="submit">{% trans "Save" %}</button></div>
|
<div class="field"><button class="button is-primary" type="submit">{% trans "Save" %}</button></div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -12,7 +12,8 @@
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<li>
|
||||||
{% url 'prefs-profile' as url %}
|
{% url 'prefs-profile' as url %}
|
||||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Profile" %}</a>
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Edit Profile" %}</a>
|
||||||
|
{% block profile-tabs %}{% endblock %}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{% url 'prefs-password' as url %}
|
{% url 'prefs-password' as url %}
|
||||||
|
|
|
@ -8,7 +8,24 @@
|
||||||
<ul class="block">
|
<ul class="block">
|
||||||
{% for result in local_results.results %}
|
{% for result in local_results.results %}
|
||||||
<li class="pd-4 mb-5">
|
<li class="pd-4 mb-5">
|
||||||
{% include 'snippets/search_result_text.html' with result=result %}
|
<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' %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-10 ml-3">
|
||||||
|
<p>
|
||||||
|
<strong>
|
||||||
|
{% include "snippets/book_titleby.html" with book=result %}
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% if result.first_published_date or result.published_date %}
|
||||||
|
({% firstof result.first_published_date.year result.published_date.year %})
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -43,7 +60,33 @@
|
||||||
<ul class="is-flex-grow-1">
|
<ul class="is-flex-grow-1">
|
||||||
{% for result in result_set.results %}
|
{% for result in result_set.results %}
|
||||||
<li class="mb-5">
|
<li class="mb-5">
|
||||||
{% include 'snippets/search_result_text.html' with result=result remote_result=True %}
|
<div class="columns is-mobile is-gapless">
|
||||||
|
<div class="columns is-mobile is-gapless">
|
||||||
|
{% include 'snippets/book_cover.html' with book=result cover_class='is-w-xs is-h-xs' external_path=True %}
|
||||||
|
</div>
|
||||||
|
<div class="column is-10 ml-3">
|
||||||
|
<p>
|
||||||
|
<strong>
|
||||||
|
<a
|
||||||
|
href="{{ result.view_link|default:result.key }}"
|
||||||
|
rel="noopener"
|
||||||
|
target="_blank"
|
||||||
|
>{{ result.title }}</a>
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ result.author }}
|
||||||
|
{% if result.year %}({{ result.year }}){% endif %}
|
||||||
|
</p>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
<form name="edit-announcement" method="post" action="{% url 'settings-announcements' announcement.id %}" class="block">
|
<form name="edit-announcement" method="post" action="{% url 'settings-announcements' announcement.id %}" class="block">
|
||||||
{% include 'settings/announcement_form.html' with controls_text="edit_announcement" %}
|
{% include 'settings/announcements/announcement_form.html' with controls_text="edit_announcement" %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="block content">
|
<div class="block content">
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
<form name="create-announcement" method="post" action="{% url 'settings-announcements' %}" class="block">
|
<form name="create-announcement" method="post" action="{% url 'settings-announcements' %}" class="block">
|
||||||
{% include 'settings/announcement_form.html' with controls_text="create_announcement" %}
|
{% include 'settings/announcements/announcement_form.html' with controls_text="create_announcement" %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
|
@ -48,11 +48,10 @@
|
||||||
<td>{% if announcement.active %}{% trans "active" %}{% else %}{% trans "inactive" %}{% endif %}</td>
|
<td>{% if announcement.active %}{% trans "active" %}{% else %}{% trans "inactive" %}{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
|
||||||
|
|
||||||
{% if not announcements %}
|
{% if not announcements %}
|
||||||
<p><em>{% trans "No announcements found." %}</em></p>
|
<tr><td colspan="5"><em>{% trans "No announcements found" %}</em></td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'snippets/pagination.html' with page=announcements path=request.path %}
|
{% include 'snippets/pagination.html' with page=announcements path=request.path %}
|
|
@ -67,27 +67,27 @@
|
||||||
<form method="get" action="{% url 'settings-dashboard' %}" class="notification has-background-white-bis">
|
<form method="get" action="{% url 'settings-dashboard' %}" class="notification has-background-white-bis">
|
||||||
<div class="is-flex is-align-items-flex-end">
|
<div class="is-flex is-align-items-flex-end">
|
||||||
<div class="ml-1 mr-1">
|
<div class="ml-1 mr-1">
|
||||||
<label class="label">
|
<label class="label" for="id_start">
|
||||||
{% trans "Start date:" %}
|
{% trans "Start date:" %}
|
||||||
<input class="input" type="date" name="start" value="{{ start }}">
|
|
||||||
</label>
|
</label>
|
||||||
|
<input class="input" type="date" name="start" value="{{ start }}" id="id_start">
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-1 mr-1">
|
<div class="ml-1 mr-1">
|
||||||
<label class="label">
|
<label class="label" for="id_end">
|
||||||
{% trans "End date:" %}
|
{% trans "End date:" %}
|
||||||
<input class="input" type="date" name="end" value="{{ end }}">
|
|
||||||
</label>
|
</label>
|
||||||
|
<input class="input" type="date" name="end" value="{{ end }}" id="id_end">
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-1 mr-1">
|
<div class="ml-1 mr-1">
|
||||||
<label class="label">
|
<label class="label" for="id_interval">
|
||||||
{% trans "Interval:" %}
|
{% trans "Interval:" %}
|
||||||
|
</label>
|
||||||
<div class="select">
|
<div class="select">
|
||||||
<select name="days">
|
<select name="days" id="id_interval">
|
||||||
<option value="1" {% if interval == 1 %}selected{% endif %}>{% trans "Days" %}</option>
|
<option value="1" {% if interval == 1 %}selected{% endif %}>{% trans "Days" %}</option>
|
||||||
<option value="7" {% if interval == 7 %}selected{% endif %}>{% trans "Weeks" %}</option>
|
<option value="7" {% if interval == 7 %}selected{% endif %}>{% trans "Weeks" %}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-1 mr-1">
|
<div class="ml-1 mr-1">
|
||||||
<button class="button is-link" type="submit">{% trans "Submit" %}</button>
|
<button class="button is-link" type="submit">{% trans "Submit" %}</button>
|
||||||
|
@ -115,6 +115,6 @@
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script>
|
||||||
{% include 'settings/dashboard_user_chart.html' %}
|
{% include 'settings/dashboard/dashboard_user_chart.html' %}
|
||||||
{% include 'settings/dashboard_status_chart.html' %}
|
{% include 'settings/dashboard/dashboard_status_chart.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -12,7 +12,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
{% include 'settings/domain_form.html' with controls_text="add_domain" class="block" %}
|
{% include 'settings/email_blocklist/domain_form.html' with controls_text="add_domain" class="block" %}
|
||||||
|
|
||||||
<p class="notification block">
|
<p class="notification block">
|
||||||
{% trans "When someone tries to register with an email from this domain, no account will be created. The registration process will appear to have worked." %}
|
{% trans "When someone tries to register with an email from this domain, no account will be created. The registration process will appear to have worked." %}
|
||||||
|
@ -55,7 +55,11 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if not domains.exists %}
|
||||||
|
<tr><td colspan="5"><em>{% trans "No email domains currently blocked" %}</em></td></tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -33,6 +33,8 @@
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-half">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_status">{% trans "Status:" %}</label>
|
<label class="label" for="id_status">{% trans "Status:" %}</label>
|
||||||
<div class="select">
|
<div class="select">
|
||||||
|
@ -43,6 +45,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columns">
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_application_type">{% trans "Software:" %}</label>
|
<label class="label" for="id_application_type">{% trans "Software:" %}</label>
|
||||||
|
@ -51,6 +55,8 @@
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-half">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_application_version">{% trans "Version:" %}</label>
|
<label class="label" for="id_application_version">{% trans "Version:" %}</label>
|
||||||
<input type="text" name="application_version" maxlength="255" class="input" id="id_application_version" value="{{ form.application_version.value|default:'' }}">
|
<input type="text" name="application_version" maxlength="255" class="input" id="id_application_version" value="{{ form.application_version.value|default:'' }}">
|
||||||
|
@ -62,7 +68,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_notes">{% trans "Notes:" %}</label>
|
<label class="label" for="id_notes">{% trans "Notes:" %}</label>
|
||||||
<textarea name="notes" cols="None" rows="None" class="textarea" id="id_notes">{{ form.notes.value|default:'' }}</textarea>
|
<textarea name="notes" cols="40" rows="5" class="textarea" id="id_notes">{{ form.notes.value|default:'' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
|
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
|
|
@ -19,18 +19,14 @@
|
||||||
<h2 class="title is-4">{% trans "Details" %}</h2>
|
<h2 class="title is-4">{% trans "Details" %}</h2>
|
||||||
<div class="box is-flex-grow-1 content">
|
<div class="box is-flex-grow-1 content">
|
||||||
<dl>
|
<dl>
|
||||||
<div class="is-flex">
|
<dt class="is-pulled-left mr-5">{% trans "Software:" %}</dt>
|
||||||
<dt>{% trans "Software:" %}</dt>
|
|
||||||
<dd>{{ server.application_type }}</dd>
|
<dd>{{ server.application_type }}</dd>
|
||||||
</div>
|
|
||||||
<div class="is-flex">
|
<dt class="is-pulled-left mr-5">{% trans "Version:" %}</dt>
|
||||||
<dt>{% trans "Version:" %}</dt>
|
|
||||||
<dd>{{ server.application_version }}</dd>
|
<dd>{{ server.application_version }}</dd>
|
||||||
</div>
|
|
||||||
<div class="is-flex">
|
<dt class="is-pulled-left mr-5">{% trans "Status:" %}</dt>
|
||||||
<dt>{% trans "Status:" %}</dt>
|
|
||||||
<dd>{{ server.get_status_display }}</dd>
|
<dd>{{ server.get_status_display }}</dd>
|
||||||
</div>
|
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -39,38 +35,32 @@
|
||||||
<h2 class="title is-4">{% trans "Activity" %}</h2>
|
<h2 class="title is-4">{% trans "Activity" %}</h2>
|
||||||
<div class="box is-flex-grow-1 content">
|
<div class="box is-flex-grow-1 content">
|
||||||
<dl>
|
<dl>
|
||||||
<div class="is-flex">
|
<dt class="is-pulled-left mr-5">{% trans "Users:" %}</dt>
|
||||||
<dt>{% trans "Users:" %}</dt>
|
|
||||||
<dd>
|
<dd>
|
||||||
{{ users.count }}
|
{{ users.count }}
|
||||||
{% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
|
{% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
|
||||||
<div class="is-flex">
|
<dt class="is-pulled-left mr-5">{% trans "Reports:" %}</dt>
|
||||||
<dt>{% trans "Reports:" %}</dt>
|
|
||||||
<dd>
|
<dd>
|
||||||
{{ reports.count }}
|
{{ reports.count }}
|
||||||
{% if reports.count %}(<a href="{% url 'settings-reports' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
|
{% if reports.count %}(<a href="{% url 'settings-reports' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
|
||||||
<div class="is-flex">
|
<dt class="is-pulled-left mr-5">{% trans "Followed by us:" %}</dt>
|
||||||
<dt>{% trans "Followed by us:" %}</dt>
|
|
||||||
<dd>
|
<dd>
|
||||||
{{ followed_by_us.count }}
|
{{ followed_by_us.count }}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
|
||||||
<div class="is-flex">
|
<dt class="is-pulled-left mr-5">{% trans "Followed by them:" %}</dt>
|
||||||
<dt>{% trans "Followed by them:" %}</dt>
|
|
||||||
<dd>
|
<dd>
|
||||||
{{ followed_by_them.count }}
|
{{ followed_by_them.count }}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
|
||||||
<div class="is-flex">
|
<dt class="is-pulled-left mr-5">{% trans "Blocked by us:" %}</dt>
|
||||||
<dt>{% trans "Blocked by us:" %}</dt>
|
|
||||||
<dd>
|
<dd>
|
||||||
{{ blocked_by_us.count }}
|
{{ blocked_by_us.count }}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -86,14 +76,13 @@
|
||||||
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit_notes" %}
|
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit_notes" %}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{% if server.notes %}
|
{% trans "<em>No notes</em>" as null_text %}
|
||||||
<div class="box" id="hide_edit_notes">{{ server.notes|to_markdown|safe }}</div>
|
<div class="box" id="hide_edit_notes">{{ server.notes|to_markdown|default:null_text|safe }}</div>
|
||||||
{% endif %}
|
|
||||||
<form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit_notes">
|
<form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit_notes">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<p>
|
<p>
|
||||||
<label class="is-sr-only" for="id_notes">Notes:</label>
|
<label class="is-sr-only" for="id_notes">Notes:</label>
|
||||||
<textarea name="notes" cols="None" rows="None" class="textarea" id="id_notes">{{ server.notes|default:"" }}</textarea>
|
<textarea name="notes" cols="40" rows="5" class="textarea" id="id_notes">{{ server.notes|default:"" }}</textarea>
|
||||||
</p>
|
</p>
|
||||||
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
|
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
|
||||||
{% trans "Cancel" as button_text %}
|
{% trans "Cancel" as button_text %}
|
|
@ -59,7 +59,11 @@
|
||||||
<td>{{ server.get_status_display }}</td>
|
<td>{{ server.get_status_display }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if not servers %}
|
||||||
|
<tr><td colspan="5"><em>{% trans "No instances found" %}</em></td></tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
{% include 'snippets/pagination.html' with page=servers path=request.path %}
|
{% include 'snippets/pagination.html' with page=servers path=request.path %}
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
||||||
|
|
||||||
{% block filter_fields %}
|
{% block filter_fields %}
|
||||||
{% include 'settings/status_filter.html' %}
|
{% include 'settings/invites/status_filter.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
{% endif %} ({{ count }})
|
{% endif %} ({{ count }})
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{% include 'settings/invite_request_filters.html' %}
|
{% include 'settings/invites/invite_request_filters.html' %}
|
||||||
|
|
||||||
<table class="table is-striped is-fullwidth">
|
<table class="table is-striped is-fullwidth">
|
||||||
{% url 'settings-invite-requests' as url %}
|
{% url 'settings-invite-requests' as url %}
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
<th>{% trans "Action" %}</th>
|
<th>{% trans "Action" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% if not requests %}
|
{% if not requests %}
|
||||||
<tr><td colspan="4">{% trans "No requests" %}</td></tr>
|
<tr><td colspan="5"><em>{% trans "No requests" %}</em></td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for req in requests %}
|
{% for req in requests %}
|
||||||
<tr>
|
<tr>
|
|
@ -12,7 +12,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
{% include 'settings/ip_address_form.html' with controls_text="add_address" class="block" %}
|
{% include 'settings/ip_blocklist/ip_address_form.html' with controls_text="add_address" class="block" %}
|
||||||
|
|
||||||
<p class="notification block">
|
<p class="notification block">
|
||||||
{% trans "Any traffic from this IP address will get a 404 response when trying to access any part of the application." %}
|
{% trans "Any traffic from this IP address will get a 404 response when trying to access any part of the application." %}
|
||||||
|
@ -42,6 +42,9 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if not addresses.exists %}
|
||||||
|
<tr><td colspan="2"><em>{% trans "No IP addresses currently blocked" %}</em></td></tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -74,14 +74,7 @@
|
||||||
<li>
|
<li>
|
||||||
{% url 'settings-site' as url %}
|
{% url 'settings-site' as url %}
|
||||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Site Settings" %}</a>
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Site Settings" %}</a>
|
||||||
{% if url in request.path %}
|
{% block site-subtabs %}{% endblock %}
|
||||||
<ul class="emnu-list">
|
|
||||||
<li><a href="{{ url }}#instance-info">{% trans "Instance Info" %}</a></li>
|
|
||||||
<li><a href="{{ url }}#images">{% trans "Images" %}</a></li>
|
|
||||||
<li><a href="{{ url }}#footer">{% trans "Footer Content" %}</a></li>
|
|
||||||
<li><a href="{{ url }}#registration">{% trans "Registration" %}</a></li>
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -3,20 +3,21 @@
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
{% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}
|
{% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}
|
||||||
{% block header %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}
|
|
||||||
|
{% block header %}
|
||||||
|
{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}
|
||||||
|
<a href="{% url 'settings-reports' %}" class="has-text-weight-normal help">{% trans "Back to reports" %}</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
<div class="block">
|
|
||||||
<a href="{% url 'settings-reports' %}">{% trans "Back to reports" %}</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{% include 'moderation/report_preview.html' with report=report %}
|
{% include 'settings/reports/report_preview.html' with report=report %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'user_admin/user_info.html' with user=report.user %}
|
{% include 'settings/users/user_info.html' with user=report.user %}
|
||||||
|
|
||||||
{% include 'user_admin/user_moderation_actions.html' with user=report.user %}
|
{% include 'settings/users/user_moderation_actions.html' with user=report.user %}
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3>
|
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3>
|
|
@ -30,7 +30,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'user_admin/user_admin_filters.html' %}
|
{% include 'settings/users/user_admin_filters.html' %}
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{% if not reports %}
|
{% if not reports %}
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
|
|
||||||
{% for report in reports %}
|
{% for report in reports %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{% include 'moderation/report_preview.html' with report=report %}
|
{% include 'settings/reports/report_preview.html' with report=report %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
|
@ -5,12 +5,21 @@
|
||||||
|
|
||||||
{% block header %}{% trans "Site Settings" %}{% endblock %}
|
{% block header %}{% trans "Site Settings" %}{% endblock %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block site-subtabs %}
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li><a href="#instance-info">{% trans "Instance Info" %}</a></li>
|
||||||
|
<li><a href="#images">{% trans "Images" %}</a></li>
|
||||||
|
<li><a href="#footer">{% trans "Footer Content" %}</a></li>
|
||||||
|
<li><a href="#registration">{% trans "Registration" %}</a></li>
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
<form action="{% url 'settings-site' %}" method="POST" class="content" enctype="multipart/form-data">
|
<form action="{% url 'settings-site' %}" method="POST" class="content" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<section class="block" id="instance_info">
|
<section class="block" id="instance_info">
|
||||||
<h2 class="title is-4">{% trans "Instance Info" %}</h2>
|
<h2 class="title is-4">{% trans "Instance Info" %}</h2>
|
||||||
|
<div class="box">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_name">{% trans "Instance Name:" %}</label>
|
<label class="label" for="id_name">{% trans "Instance Name:" %}</label>
|
||||||
{{ site_form.name }}
|
{{ site_form.name }}
|
||||||
|
@ -36,22 +45,23 @@
|
||||||
<label class="label" for="id_privacy_policy">{% trans "Privacy Policy:" %}</label>
|
<label class="label" for="id_privacy_policy">{% trans "Privacy Policy:" %}</label>
|
||||||
{{ site_form.privacy_policy }}
|
{{ site_form.privacy_policy }}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr aria-hidden="true">
|
<hr aria-hidden="true">
|
||||||
|
|
||||||
<section class="block" id="images">
|
<section class="block" id="images">
|
||||||
<h2 class="title is-4">{% trans "Images" %}</h2>
|
<h2 class="title is-4">{% trans "Images" %}</h2>
|
||||||
<div class="columns">
|
<div class="box is-flex">
|
||||||
<div class="column">
|
<div>
|
||||||
<label class="label" for="id_logo">{% trans "Logo:" %}</label>
|
<label class="label" for="id_logo">{% trans "Logo:" %}</label>
|
||||||
{{ site_form.logo }}
|
{{ site_form.logo }}
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div>
|
||||||
<label class="label" for="id_logo_small">{% trans "Logo small:" %}</label>
|
<label class="label" for="id_logo_small">{% trans "Logo small:" %}</label>
|
||||||
{{ site_form.logo_small }}
|
{{ site_form.logo_small }}
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div>
|
||||||
<label class="label" for="id_favicon">{% trans "Favicon:" %}</label>
|
<label class="label" for="id_favicon">{% trans "Favicon:" %}</label>
|
||||||
{{ site_form.favicon }}
|
{{ site_form.favicon }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,6 +72,7 @@
|
||||||
|
|
||||||
<section class="block" id="footer">
|
<section class="block" id="footer">
|
||||||
<h2 class="title is-4">{% trans "Footer Content" %}</h2>
|
<h2 class="title is-4">{% trans "Footer Content" %}</h2>
|
||||||
|
<div class="box">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_support_link">{% trans "Support link:" %}</label>
|
<label class="label" for="id_support_link">{% trans "Support link:" %}</label>
|
||||||
<input type="text" name="support_link" maxlength="255" class="input" id="id_support_link" placeholder="https://www.patreon.com/bookwyrm"{% if site.support_link %} value="{{ site.support_link }}"{% endif %}>
|
<input type="text" name="support_link" maxlength="255" class="input" id="id_support_link" placeholder="https://www.patreon.com/bookwyrm"{% if site.support_link %} value="{{ site.support_link }}"{% endif %}>
|
||||||
|
@ -78,12 +89,14 @@
|
||||||
<label class="label" for="id_footer_item">{% trans "Additional info:" %}</label>
|
<label class="label" for="id_footer_item">{% trans "Additional info:" %}</label>
|
||||||
{{ site_form.footer_item }}
|
{{ site_form.footer_item }}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr aria-hidden="true">
|
<hr aria-hidden="true">
|
||||||
|
|
||||||
<section class="block" id="registration">
|
<section class="block" id="registration">
|
||||||
<h2 class="title is-4">{% trans "Registration" %}</h2>
|
<h2 class="title is-4">{% trans "Registration" %}</h2>
|
||||||
|
<div class="box">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_allow_registration">
|
<label class="label" for="id_allow_registration">
|
||||||
{{ site_form.allow_registration }}
|
{{ site_form.allow_registration }}
|
||||||
|
@ -97,7 +110,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label mb-0" for="id_allow_invite_requests">
|
<label class="label mb-0" for="id_require_confirm_email">
|
||||||
{{ site_form.require_confirm_email }}
|
{{ site_form.require_confirm_email }}
|
||||||
{% trans "Require users to confirm email address" %}
|
{% trans "Require users to confirm email address" %}
|
||||||
</label>
|
</label>
|
||||||
|
@ -114,6 +127,7 @@
|
||||||
<p class="help is-danger">{{ error|escape }}</p>
|
<p class="help is-danger">{{ error|escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="block">
|
<footer class="block">
|
||||||
|
|
16
bookwyrm/templates/settings/users/user.html
Normal file
16
bookwyrm/templates/settings/users/user.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends 'settings/layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{{ user.username }}{% endblock %}
|
||||||
|
{% block header %}
|
||||||
|
{{ user.username }}
|
||||||
|
<a class="help has-text-weight-normal" href="{% url 'settings-users' %}">{% trans "Back to users" %}</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
{% include 'settings/users/user_info.html' with user=user %}
|
||||||
|
|
||||||
|
{% include 'settings/users/user_moderation_actions.html' with user=user %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
{% include 'user_admin/user_admin_filters.html' %}
|
{% include 'settings/users/user_admin_filters.html' %}
|
||||||
|
|
||||||
<table class="table is-striped">
|
<table class="table is-striped">
|
||||||
<tr>
|
<tr>
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
||||||
|
|
||||||
{% block filter_fields %}
|
{% block filter_fields %}
|
||||||
{% include 'user_admin/username_filter.html' %}
|
{% include 'settings/users/username_filter.html' %}
|
||||||
{% include 'directory/community_filter.html' %}
|
{% include 'directory/community_filter.html' %}
|
||||||
{% include 'user_admin/server_filter.html' %}
|
{% include 'settings/users/server_filter.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue