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 'no_autofocus: 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

View file

@ -8,4 +8,4 @@ WORKDIR /app
COPY requirements.txt /app/
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 = ""
pages: int = None
physicalFormat: str = ""
physicalFormatDetail: str = ""
publishers: List[str] = field(default_factory=lambda: [])
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 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 """
from abc import ABC, abstractmethod
from dataclasses import asdict, dataclass
import logging
from django.db import transaction
@ -9,6 +8,7 @@ from requests.exceptions import RequestException
from bookwyrm import activitypub, models, settings
from .connector_manager import load_more_data, ConnectorException
from .format_mappings import format_mappings
logger = logging.getLogger(__name__)
@ -31,7 +31,6 @@ class AbstractMinimalConnector(ABC):
"isbn_search_url",
"name",
"identifier",
"local",
]
for field in self_fields:
setattr(self, field, getattr(info, field))
@ -267,32 +266,6 @@ def get_image(url, timeout=10):
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:
"""associate a local database field with a field in an external dataset"""
@ -312,3 +285,25 @@ class Mapping:
return self.formatter(value)
except: # pylint: disable=bare-except
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 """
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):

View file

@ -10,7 +10,7 @@ from django.db.models import signals
from requests import HTTPError
from bookwyrm import models
from bookwyrm import book_search, models
from bookwyrm.tasks import app
logger = logging.getLogger(__name__)
@ -55,7 +55,7 @@ def search(query, min_confidence=0.1, return_first=False):
# if we found anything, return it
return result_set[0]
if result_set or connector.local:
if result_set:
results.append(
{
"connector": connector,
@ -71,22 +71,13 @@ def search(query, min_confidence=0.1, return_first=False):
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):
"""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

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
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 .connector_manager import ConnectorException
class Connector(AbstractConnector):
"""instantiate a connector for OL"""
"""instantiate a connector for inventaire"""
def __init__(self, identifier):
super().__init__(identifier)

View file

@ -2,8 +2,9 @@
import re
from bookwyrm import models
from .abstract_connector import AbstractConnector, SearchResult, Mapping
from .abstract_connector import get_data
from bookwyrm.book_search import SearchResult
from .abstract_connector import AbstractConnector, Mapping
from .abstract_connector import get_data, infer_physical_format, unique_physical_format
from .connector_manager import ConnectorException
from .openlibrary_languages import languages
@ -43,7 +44,16 @@ class Connector(AbstractConnector):
),
Mapping("publishedDate", remote_field="publish_date"),
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"),
]

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 """
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
if isinstance(visible.field.widget, Textarea):
input_type = "textarea"
visible.field.widget.attrs["cols"] = None
visible.field.widget.attrs["rows"] = None
visible.field.widget.attrs["rows"] = 5
visible.field.widget.attrs["class"] = css_classes[input_type]
@ -228,7 +227,7 @@ class ExpiryWidget(widgets.Select):
elif selected_string == "forever":
return None
else:
return selected_string # "This will raise
return selected_string # This will raise
return timezone.now() + interval
@ -269,7 +268,7 @@ class CreateInviteForm(CustomForm):
class ShelfForm(CustomForm):
class Meta:
model = models.Shelf
fields = ["user", "name", "privacy"]
fields = ["user", "name", "privacy", "description"]
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 bookwyrm.models import Connector, FederatedServer, SiteSettings, User
from bookwyrm.settings import DOMAIN
def init_groups():
@ -73,19 +72,6 @@ def init_permissions():
def init_connectors():
"""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(
identifier="bookwyrm.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 """
import base64
from Crypto import Random
from django.core.exceptions import PermissionDenied
from django.db import models
from django.dispatch import receiver
from django.http import Http404
from django.utils.translation import gettext_lazy as _
from bookwyrm.settings import DOMAIN
@ -48,26 +51,26 @@ class BookWyrmModel(models.Model):
"""how to link to this object in the local app"""
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?"""
# make sure this is an object with privacy owned by a user
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
if viewer in self.user.blocks.all():
return False
raise Http404()
# you can see your own posts and any public or unlisted posts
if viewer == self.user or self.privacy in ["public", "unlisted"]:
return True
return
# you can see the followers only posts of people you follow
if (
self.privacy == "followers"
and self.user.followers.filter(id=viewer.id).first()
):
return True
return
# you can see dms you are tagged in
if hasattr(self, "mention_users"):
@ -75,6 +78,7 @@ class BookWyrmModel(models.Model):
self.privacy == "direct"
and self.mention_users.filter(id=viewer.id).first()
):
return True
# you can see groups of which you are a member
@ -89,7 +93,31 @@ class BookWyrmModel(models.Model):
):
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)

View file

@ -3,9 +3,10 @@ import re
from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex
from django.db import models
from django.db import transaction
from django.db import models, transaction
from django.db.models import Prefetch
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from model_utils import FieldTracker
from model_utils.managers import InheritanceManager
from imagekit.models import ImageSpecField
@ -226,6 +227,16 @@ class Work(OrderedCollectionPageMixin, Book):
deserialize_reverse_fields = [("editions", "editions")]
# https://schema.org/BookFormatType
FormatChoices = [
("AudiobookFormat", _("Audiobook")),
("EBook", _("eBook")),
("GraphicNovel", _("Graphic novel")),
("Hardcover", _("Hardcover")),
("Paperback", _("Paperback")),
]
class Edition(Book):
"""an edition of a book"""
@ -243,7 +254,10 @@ class Edition(Book):
max_length=255, blank=True, null=True, deduplication_field=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(
models.CharField(max_length=255), blank=True, default=list
)
@ -307,6 +321,27 @@ class Edition(Book):
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):
"""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)
priority = models.IntegerField(default=2)
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)
api_key = models.CharField(max_length=255, null=True, blank=True)
active = models.BooleanField(default=True)

View file

@ -101,6 +101,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
notification_type="ADD",
)
<<<<<<< HEAD
if self.book_list.group:
for membership in self.book_list.group.memberships.all():
if membership.user != self.user:
@ -110,6 +111,14 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
related_list_item=self,
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:
"""A book may only be placed into a list 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)
finish_date = models.DateTimeField(blank=True, null=True)
is_active = models.BooleanField(default=True)
def save(self, *args, **kwargs):
"""update user active time"""
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)
def create_update(self):

View file

@ -1,5 +1,6 @@
""" puttin' books on shelves """
import re
from django.core.exceptions import PermissionDenied
from django.db import models
from django.utils import timezone
@ -20,6 +21,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
name = fields.CharField(max_length=100)
identifier = models.CharField(max_length=100)
description = models.TextField(blank=True, null=True, max_length=500)
user = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="owner"
)
@ -51,12 +53,23 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
"""list of books for this shelf, overrides OrderedCollectionMixin"""
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):
"""shelf identifier instead of id"""
base_path = self.user.remote_id
identifier = self.identifier or self.get_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:
"""user/shelf unqiueness"""

View file

@ -3,6 +3,7 @@ from dataclasses import MISSING
import re
from django.apps import apps
from django.core.exceptions import PermissionDenied
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.dispatch import receiver
@ -56,6 +57,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
on_delete=models.PROTECT,
activitypub_field="inReplyTo",
)
thread_id = models.IntegerField(blank=True, null=True)
objects = InheritanceManager()
activity_serializer = activitypub.Note
@ -67,6 +69,17 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
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
""" "delete" a status"""
if hasattr(self, "boosted_status"):
@ -187,6 +200,13 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
"""json serialized activitypub class"""
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):
"""these are app-generated messages about user activity"""

View file

@ -13,7 +13,7 @@ VERSION = "0.0.1"
PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
JS_CACHE = "7f2343cf"
JS_CACHE = "c02929b1"
# email
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
* @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();
// Update shelve buttons
if (form.reading_status) {
document.querySelectorAll("[data-shelve-button-book='" + form.book.value +"']")
.forEach(button => this.cycleShelveButtons(button, form.reading_status.value));
}
return;
}

View file

@ -22,7 +22,7 @@
</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 }}">
{% 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">
<section class="box">
{% with 0|uuid as controls_uid %}
{% include 'snippets/create_status.html' with book=book hide_cover=True %}
{% endwith %}
</section>
{% endif %}
<div class="block" id="reviews">

View file

@ -1,6 +1,7 @@
{% spaceless %}
{% load i18n %}
{% if book.isbn13 or book.oclc_number or book.asin %}
<dl>
{% if book.isbn_13 %}
<div class="is-flex">
@ -23,4 +24,5 @@
</div>
{% endif %}
</dl>
{% endif %}
{% 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 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 %}
<div class="block">
@ -42,87 +6,14 @@
</div>
{% endif %}
<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"
>
{% csrf_token %}
{% csrf_token %}
{% 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 %}
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
<div class="columns">
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
<div class="columns">
<div class="column is-half">
<section class="block">
<h2 class="title is-4">{% trans "Metadata" %}</h2>
<div class="box">
<div class="field">
<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">
@ -147,6 +38,8 @@
{% endfor %}
</div>
<div class="columns">
<div class="column is-two-thirds">
<div class="field">
<label class="label" for="id_series">{% trans "Series:" %}</label>
<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>
{% endfor %}
</div>
</div>
<div class="column is-one-third">
<div class="field">
<label class="label" for="id_series_number">{% trans "Series number:" %}</label>
{{ form.series_number }}
@ -162,6 +56,8 @@
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
</div>
<div class="field">
<label class="label" for="id_languages">{% trans "Languages:" %}</label>
@ -171,7 +67,12 @@
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
</section>
<section class="block">
<h2 class="title is-4">{% trans "Publication" %}</h2>
<div class="box">
<div class="field">
<label class="label" for="id_publishers">{% trans "Publisher:" %}</label>
{{ form.publishers }}
@ -196,10 +97,12 @@
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
</section>
<section class="block">
<h2 class="title is-4">{% trans "Authors" %}</h2>
<div class="box">
{% if book.authors.exists %}
<fieldset>
{% 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 %}>
<span class="help">{% trans "Separate multiple values with commas." %}</span>
</div>
</div>
</section>
</div>
<div class="column is-half">
<section class="block">
<h2 class="title is-4">{% trans "Cover" %}</h2>
<div class="box">
<div class="columns">
{% if book.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' %}
</div>
{% endif %}
<div class="column">
<div class="block">
<div class="field">
<label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
{{ form.cover }}
</div>
{% if book %}
<div class="field">
<label class="label" for="id_cover_url">
{% trans "Load cover from url:" %}
</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>
{% endif %}
{% for error in form.cover.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
</div>
</section>
<div class="block">
<section class="block">
<h2 class="title is-4">{% trans "Physical Properties" %}</h2>
<div class="box">
<div class="columns">
<div class="column is-one-third">
<div class="field">
<label class="label" for="id_physical_format">{% trans "Format:" %}</label>
<div class="select">
{{ form.physical_format }}
</div>
{% for error in form.physical_format.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</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">
<label class="label" for="id_pages">{% trans "Pages:" %}</label>
@ -269,9 +191,11 @@
{% endfor %}
</div>
</div>
</section>
<div class="block">
<section class="block">
<h2 class="title is-4">{% trans "Book Identifiers" %}</h2>
<div class="box">
<div class="field">
<label class="label" for="id_isbn_13">{% trans "ISBN 13:" %}</label>
{{ form.isbn_13 }}
@ -320,15 +244,6 @@
{% endfor %}
</div>
</div>
</section>
</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 %}
</div>

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>
</div>
{% include 'book/edition_filters.html' %}
{% include 'book/editions/edition_filters.html' %}
<div class="block">
{% for book in editions %}

View file

@ -3,20 +3,20 @@
{% load i18n %}
{% 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>
{% 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 %}
{% blocktrans %}{{ format }}{% endblocktrans %}
{% elif format and pages %}
@ -24,8 +24,9 @@
{% elif pages %}
{% blocktrans %}{{ pages }} pages{% endblocktrans %}
{% endif %}
{% endwith %}
</p>
{% endif %}
{% endwith %}
{% if book.languages %}
{% for language in book.languages %}
@ -39,14 +40,15 @@
</p>
{% endif %}
<p>
{% with date=book.published_date|naturalday publisher=book.publishers|join:', ' %}
{% if date or book.first_published_date %}
{% 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 %}
<meta
itemprop="datePublished"
content="{{ book.first_published_date|default:book.published_date|date:'Y-m-d' }}"
>
{% endif %}
{% endif %}
<p>
{% comment %}
@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 %}
{% blocktrans %}Published by {{ publisher }}.{% endblocktrans %}
{% endif %}
{% endwith %}
</p>
{% endif %}
{% endwith %}
{% endspaceless %}

View file

@ -1,5 +1,5 @@
{% 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">
<h2 class="card-header-title" tabindex="0" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}_header">
{% block header %}{% endblock %}

View file

@ -8,7 +8,7 @@
{% trans "Local users" %}
</label>
<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" %}
</label>
{% endblock %}

View file

@ -5,8 +5,8 @@
<label class="label" for="id_sort">{% trans "Order by" %}</label>
<div class="select">
<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 == "suggested" %}checked{% endif %}>{% trans "Recently active" %}</option>
<option value="recent" {% if request.GET.sort == "recent" %}selected{% endif %}>{% trans "Recently active" %}</option>
<option value="suggested" {% if request.GET.sort == "suggested" %}selected{% endif %}>{% trans "Suggested" %}</option>
</select>
</div>
{% endblock %}

View file

@ -25,7 +25,7 @@
{% if request.user.show_goal and not goal and tab.key == 'home' %}
{% now 'Y' as year %}
<section class="block">
{% include 'snippets/goal_card.html' with year=year %}
{% include 'feed/goal_card.html' with year=year %}
<hr>
</section>
{% endif %}

View file

@ -7,13 +7,8 @@
</h3>
{% endblock %}
{% block card-content %}
<div class="content">
<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>
{% include 'snippets/goal_form.html' %}
{% endblock %}
{% block card-footer %}

View file

@ -1,6 +1,7 @@
{% load status_display %}
<div class="block">
<div class="thread-parent is-relative block">
<div class="thread">
{% with depth=depth|add:1 %}
{% if depth <= max_depth and status.reply_parent and direction <= 0 %}
{% with direction=-1 %}
@ -8,7 +9,9 @@
{% endwith %}
{% 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 %}
{% for reply in status|replies %}
@ -18,5 +21,5 @@
{% endfor %}
{% endif %}
{% endwith %}
</div>
</div>

View file

@ -10,6 +10,8 @@
<link rel="stylesheet" href="{% static "css/vendor/icons.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 %}">
{% if preview_images_enabled is True %}
@ -34,7 +36,7 @@
<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">
</a>
<form class="navbar-item column" action="/search/">
<form class="navbar-item column" action="{% url 'search' %}">
<div class="field has-addons">
<div class="control">
{% if user.is_authenticated %}
@ -115,7 +117,7 @@
</a>
</li>
{% 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 %}
{% if perms.bookwyrm.create_invites and not site.allow_registration %}
<li>
@ -131,7 +133,7 @@
</a>
</li>
{% endif %}
<li class="navbar-divider" role="presentation"></li>
<li class="navbar-divider" role="presentation">&nbsp;</li>
<li>
<a href="{% url 'logout' %}" class="navbar-item">
{% 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>
</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">
<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="control">
<label for="input-list-position" class="button is-transparent is-small">{% trans "List position" %}</label>
</div>
{% csrf_token %}
<div class="control">
<input id="input_list_position" class="input is-small" type="number" min="1" name="position" value="{{ item.order }}">
</div>
@ -84,7 +84,9 @@
</div>
</form>
</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 %}
<input type="hidden" name="item" value="{{ item.id }}">
<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 %}
{% if not request.user.blocks.exists %}
<p>{% trans "No users currently blocked." %}</p>
<p><em>{% trans "No users currently blocked." %}</em></p>
{% else %}
<ul>
{% for user in request.user.blocks.all %}

View file

@ -10,11 +10,11 @@
{% block panel %}
<form name="edit-profile" action="{% url 'prefs-password' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="block">
<div class="field">
<label class="label" for="id_password">{% trans "New password:" %}</label>
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password">
</div>
<div class="block">
<div class="field">
<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">
</div>

View file

@ -7,46 +7,72 @@
{% trans "Edit Profile" %}
{% 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 %}
{% if form.non_field_errors %}
<p class="notification is-danger">{{ form.non_field_errors }}</p>
{% endif %}
<form name="edit-profile" action="{% url 'prefs-profile' %}" method="post" enctype="multipart/form-data">
{% 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>
<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 }}
{% for error in form.avatar.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
</div>
<div class="field">
<label class="label" for="id_name">{% trans "Display name:" %}</label>
{{ form.name }}
{% for error in form.name.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<div class="field">
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
{{ form.summary }}
{% for error in form.summary.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<div class="field">
<label class="label" for="id_email">{% trans "Email address:" %}</label>
{{ form.email }}
{% for error in form.email.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</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">
{% trans "Show reading goal prompt in feed:" %}
{{ form.show_goal }}
</label>
<label class="checkbox label" for="id_show_goal">
<label class="checkbox label" for="id_show_suggested_users">
{% trans "Show suggested users:" %}
{{ form.show_suggested_users }}
</label>
@ -55,15 +81,31 @@
{{ form.discoverable }}
</label>
{% 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 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">
{% trans "Manually approve followers:" %}
{{ form.manually_approves_followers }}
</label>
</div>
<div class="block">
<div class="field">
<label class="label" for="id_default_post_privacy">
{% trans "Default post privacy:" %}
</label>
@ -71,12 +113,8 @@
{{ form.default_post_privacy }}
</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 class="block"><button class="button is-primary" type="submit">{% trans "Save" %}</button></div>
</section>
<div class="field"><button class="button is-primary" type="submit">{% trans "Save" %}</button></div>
</form>
{% endblock %}

View file

@ -12,7 +12,8 @@
<ul class="menu-list">
<li>
{% 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>
{% url 'prefs-password' as url %}

View file

@ -8,7 +8,24 @@
<ul class="block">
{% for result in local_results.results %}
<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>
{% endfor %}
</ul>
@ -43,7 +60,33 @@
<ul class="is-flex-grow-1">
{% for result in result_set.results %}
<li class="mb-5">
{% include 'snippets/search_result_text.html' with result=result remote_result=True %}
<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>
{% endfor %}
</ul>

View file

@ -26,7 +26,7 @@
{% block panel %}
<form name="edit-announcement" method="post" action="{% url 'settings-announcements' announcement.id %}" class="block">
{% include 'settings/announcement_form.html' with controls_text="edit_announcement" %}
{% include 'settings/announcements/announcement_form.html' with controls_text="edit_announcement" %}
</form>
<div class="block content">

View file

@ -11,7 +11,7 @@
{% block panel %}
<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>
<div class="block">
@ -48,11 +48,10 @@
<td>{% if announcement.active %}{% trans "active" %}{% else %}{% trans "inactive" %}{% endif %}</td>
</tr>
{% endfor %}
</table>
{% if not announcements %}
<p><em>{% trans "No announcements found." %}</em></p>
<tr><td colspan="5"><em>{% trans "No announcements found" %}</em></td></tr>
{% endif %}
</table>
</div>
{% 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">
<div class="is-flex is-align-items-flex-end">
<div class="ml-1 mr-1">
<label class="label">
<label class="label" for="id_start">
{% trans "Start date:" %}
<input class="input" type="date" name="start" value="{{ start }}">
</label>
<input class="input" type="date" name="start" value="{{ start }}" id="id_start">
</div>
<div class="ml-1 mr-1">
<label class="label">
<label class="label" for="id_end">
{% trans "End date:" %}
<input class="input" type="date" name="end" value="{{ end }}">
</label>
<input class="input" type="date" name="end" value="{{ end }}" id="id_end">
</div>
<div class="ml-1 mr-1">
<label class="label">
<label class="label" for="id_interval">
{% trans "Interval:" %}
</label>
<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="7" {% if interval == 7 %}selected{% endif %}>{% trans "Weeks" %}</option>
</select>
</div>
</label>
</div>
<div class="ml-1 mr-1">
<button class="button is-link" type="submit">{% trans "Submit" %}</button>
@ -115,6 +115,6 @@
{% block scripts %}
<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_status_chart.html' %}
{% include 'settings/dashboard/dashboard_user_chart.html' %}
{% include 'settings/dashboard/dashboard_status_chart.html' %}
{% endblock %}

View file

@ -12,7 +12,7 @@
{% endblock %}
{% 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">
{% 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>
</tr>
{% endfor %}
{% if not domains.exists %}
<tr><td colspan="5"><em>{% trans "No email domains currently blocked" %}</em></td></tr>
{% endif %}
</table>
{% endblock %}

View file

@ -33,6 +33,8 @@
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
<div class="column is-half">
<div class="field">
<label class="label" for="id_status">{% trans "Status:" %}</label>
<div class="select">
@ -43,6 +45,8 @@
</div>
</div>
</div>
</div>
<div class="columns">
<div class="column is-half">
<div class="field">
<label class="label" for="id_application_type">{% trans "Software:" %}</label>
@ -51,6 +55,8 @@
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
<div class="column is-half">
<div class="field">
<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:'' }}">
@ -62,7 +68,7 @@
</div>
<div class="field">
<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>
<button type="submit" class="button is-primary">{% trans "Save" %}</button>

View file

@ -19,18 +19,14 @@
<h2 class="title is-4">{% trans "Details" %}</h2>
<div class="box is-flex-grow-1 content">
<dl>
<div class="is-flex">
<dt>{% trans "Software:" %}</dt>
<dt class="is-pulled-left mr-5">{% trans "Software:" %}</dt>
<dd>{{ server.application_type }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Version:" %}</dt>
<dt class="is-pulled-left mr-5">{% trans "Version:" %}</dt>
<dd>{{ server.application_version }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Status:" %}</dt>
<dt class="is-pulled-left mr-5">{% trans "Status:" %}</dt>
<dd>{{ server.get_status_display }}</dd>
</div>
</dl>
</div>
</section>
@ -39,38 +35,32 @@
<h2 class="title is-4">{% trans "Activity" %}</h2>
<div class="box is-flex-grow-1 content">
<dl>
<div class="is-flex">
<dt>{% trans "Users:" %}</dt>
<dt class="is-pulled-left mr-5">{% trans "Users:" %}</dt>
<dd>
{{ users.count }}
{% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
</dd>
</div>
<div class="is-flex">
<dt>{% trans "Reports:" %}</dt>
<dt class="is-pulled-left mr-5">{% trans "Reports:" %}</dt>
<dd>
{{ reports.count }}
{% if reports.count %}(<a href="{% url 'settings-reports' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
</dd>
</div>
<div class="is-flex">
<dt>{% trans "Followed by us:" %}</dt>
<dt class="is-pulled-left mr-5">{% trans "Followed by us:" %}</dt>
<dd>
{{ followed_by_us.count }}
</dd>
</div>
<div class="is-flex">
<dt>{% trans "Followed by them:" %}</dt>
<dt class="is-pulled-left mr-5">{% trans "Followed by them:" %}</dt>
<dd>
{{ followed_by_them.count }}
</dd>
</div>
<div class="is-flex">
<dt>{% trans "Blocked by us:" %}</dt>
<dt class="is-pulled-left mr-5">{% trans "Blocked by us:" %}</dt>
<dd>
{{ blocked_by_us.count }}
</dd>
</div>
</dl>
</div>
</section>
@ -86,14 +76,13 @@
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit_notes" %}
</div>
</header>
{% if server.notes %}
<div class="box" id="hide_edit_notes">{{ server.notes|to_markdown|safe }}</div>
{% endif %}
{% trans "<em>No notes</em>" as null_text %}
<div class="box" id="hide_edit_notes">{{ server.notes|to_markdown|default:null_text|safe }}</div>
<form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit_notes">
{% csrf_token %}
<p>
<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>
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
{% trans "Cancel" as button_text %}

View file

@ -59,7 +59,11 @@
<td>{{ server.get_status_display }}</td>
</tr>
{% endfor %}
{% if not servers %}
<tr><td colspan="5"><em>{% trans "No instances found" %}</em></td></tr>
{% endif %}
</table>
{% include 'snippets/pagination.html' with page=servers path=request.path %}
{% endblock %}

View file

@ -1,6 +1,6 @@
{% extends 'snippets/filters_panel/filters_panel.html' %}
{% block filter_fields %}
{% include 'settings/status_filter.html' %}
{% include 'settings/invites/status_filter.html' %}
{% endblock %}

View file

@ -26,7 +26,7 @@
{% endif %} ({{ count }})
</h2>
{% include 'settings/invite_request_filters.html' %}
{% include 'settings/invites/invite_request_filters.html' %}
<table class="table is-striped is-fullwidth">
{% url 'settings-invite-requests' as url %}
@ -47,7 +47,7 @@
<th>{% trans "Action" %}</th>
</tr>
{% if not requests %}
<tr><td colspan="4">{% trans "No requests" %}</td></tr>
<tr><td colspan="5"><em>{% trans "No requests" %}</em></td></tr>
{% endif %}
{% for req in requests %}
<tr>

View file

@ -12,7 +12,7 @@
{% endblock %}
{% 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">
{% 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>
</tr>
{% endfor %}
{% if not addresses.exists %}
<tr><td colspan="2"><em>{% trans "No IP addresses currently blocked" %}</em></td></tr>
{% endif %}
</table>
{% endblock %}

View file

@ -74,14 +74,7 @@
<li>
{% url 'settings-site' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Site Settings" %}</a>
{% if url in request.path %}
<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 %}
{% block site-subtabs %}{% endblock %}
</li>
</ul>
{% endif %}

View file

@ -3,20 +3,21 @@
{% load humanize %}
{% 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 %}
<div class="block">
<a href="{% url 'settings-reports' %}">{% trans "Back to reports" %}</a>
</div>
<div class="block">
{% include 'moderation/report_preview.html' with report=report %}
{% include 'settings/reports/report_preview.html' with report=report %}
</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">
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3>

View file

@ -30,7 +30,7 @@
</ul>
</div>
{% include 'user_admin/user_admin_filters.html' %}
{% include 'settings/users/user_admin_filters.html' %}
<div class="block">
{% if not reports %}
@ -39,7 +39,7 @@
{% for report in reports %}
<div class="block">
{% include 'moderation/report_preview.html' with report=report %}
{% include 'settings/reports/report_preview.html' with report=report %}
</div>
{% endfor %}
</div>

View file

@ -5,12 +5,21 @@
{% 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">
{% csrf_token %}
<section class="block" id="instance_info">
<h2 class="title is-4">{% trans "Instance Info" %}</h2>
<div class="box">
<div class="field">
<label class="label" for="id_name">{% trans "Instance Name:" %}</label>
{{ site_form.name }}
@ -36,22 +45,23 @@
<label class="label" for="id_privacy_policy">{% trans "Privacy Policy:" %}</label>
{{ site_form.privacy_policy }}
</div>
</div>
</section>
<hr aria-hidden="true">
<section class="block" id="images">
<h2 class="title is-4">{% trans "Images" %}</h2>
<div class="columns">
<div class="column">
<div class="box is-flex">
<div>
<label class="label" for="id_logo">{% trans "Logo:" %}</label>
{{ site_form.logo }}
</div>
<div class="column">
<div>
<label class="label" for="id_logo_small">{% trans "Logo small:" %}</label>
{{ site_form.logo_small }}
</div>
<div class="column">
<div>
<label class="label" for="id_favicon">{% trans "Favicon:" %}</label>
{{ site_form.favicon }}
</div>
@ -62,6 +72,7 @@
<section class="block" id="footer">
<h2 class="title is-4">{% trans "Footer Content" %}</h2>
<div class="box">
<div class="field">
<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 %}>
@ -78,12 +89,14 @@
<label class="label" for="id_footer_item">{% trans "Additional info:" %}</label>
{{ site_form.footer_item }}
</div>
</div>
</section>
<hr aria-hidden="true">
<section class="block" id="registration">
<h2 class="title is-4">{% trans "Registration" %}</h2>
<div class="box">
<div class="field">
<label class="label" for="id_allow_registration">
{{ site_form.allow_registration }}
@ -97,7 +110,7 @@
</label>
</div>
<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 }}
{% trans "Require users to confirm email address" %}
</label>
@ -114,6 +127,7 @@
<p class="help is-danger">{{ error|escape }}</p>
{% endfor %}
</div>
</div>
</section>
<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 %}
{% include 'user_admin/user_admin_filters.html' %}
{% include 'settings/users/user_admin_filters.html' %}
<table class="table is-striped">
<tr>

View file

@ -1,7 +1,7 @@
{% extends 'snippets/filters_panel/filters_panel.html' %}
{% block filter_fields %}
{% include 'user_admin/username_filter.html' %}
{% include 'settings/users/username_filter.html' %}
{% include 'directory/community_filter.html' %}
{% include 'user_admin/server_filter.html' %}
{% include 'settings/users/server_filter.html' %}
{% endblock %}

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