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:
Hugh Rundle 2021-10-03 10:49:38 +11:00
commit 602664b8d7
231 changed files with 8983 additions and 7361 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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",
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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),
),
]

View 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),
),
]

View 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),
]

View 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),
]

View 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",
),
]

View 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),
]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.
******************************************************************************/ ******************************************************************************/

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

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

View file

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

View file

@ -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. Well be using Thing which is the more generic ancestor. @todo The publisher property needs to be an Organization or a Person. Well 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 %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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">&nbsp;</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">&nbsp;</li>
<li> <li>
<a href="{% url 'logout' %}" class="navbar-item"> <a href="{% url 'logout' %}" class="navbar-item">
{% trans 'Log out' %} {% trans 'Log out' %}

View file

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

View file

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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