Merge branch 'main' into run-not-exec

This commit is contained in:
Mouse Reeve 2022-01-22 15:51:42 -08:00 committed by GitHub
commit ec63ca3817
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
191 changed files with 10483 additions and 5099 deletions

View file

@ -28,10 +28,14 @@ MAX_STREAM_LENGTH=200
REDIS_ACTIVITY_HOST=redis_activity REDIS_ACTIVITY_HOST=redis_activity
REDIS_ACTIVITY_PORT=6379 REDIS_ACTIVITY_PORT=6379
REDIS_ACTIVITY_PASSWORD=redispassword345 REDIS_ACTIVITY_PASSWORD=redispassword345
# Optional, use a different redis database (defaults to 0)
# REDIS_ACTIVITY_DB_INDEX=0
# Redis as celery broker # Redis as celery broker
REDIS_BROKER_PORT=6379 REDIS_BROKER_PORT=6379
REDIS_BROKER_PASSWORD=redispassword123 REDIS_BROKER_PASSWORD=redispassword123
# Optional, use a different redis database (defaults to 0)
# REDIS_BROKER_DB_INDEX=0
# Monitoring for celery # Monitoring for celery
FLOWER_PORT=8888 FLOWER_PORT=8888

View file

@ -20,22 +20,6 @@ class ActivityEncoder(JSONEncoder):
return o.__dict__ return o.__dict__
@dataclass
class Link:
"""for tagging a book in a status"""
href: str
name: str
type: str = "Link"
@dataclass
class Mention(Link):
"""a subtype of Link for mentioning an actor"""
type: str = "Mention"
@dataclass @dataclass
# pylint: disable=invalid-name # pylint: disable=invalid-name
class Signature: class Signature:
@ -198,8 +182,9 @@ class ActivityObject:
) )
return instance return instance
def serialize(self): def serialize(self, **kwargs):
"""convert to dictionary with context attr""" """convert to dictionary with context attr"""
omit = kwargs.get("omit", ())
data = self.__dict__.copy() data = self.__dict__.copy()
# recursively serialize # recursively serialize
for (k, v) in data.items(): for (k, v) in data.items():
@ -208,8 +193,9 @@ class ActivityObject:
data[k] = v.serialize() data[k] = v.serialize()
except TypeError: except TypeError:
pass pass
data = {k: v for (k, v) in data.items() if v is not None} data = {k: v for (k, v) in data.items() if v is not None and k not in omit}
data["@context"] = "https://www.w3.org/ns/activitystreams" if "@context" not in omit:
data["@context"] = "https://www.w3.org/ns/activitystreams"
return data return data
@ -222,35 +208,32 @@ def set_related_field(
model = apps.get_model(f"bookwyrm.{model_name}", require_ready=True) model = apps.get_model(f"bookwyrm.{model_name}", require_ready=True)
origin_model = apps.get_model(f"bookwyrm.{origin_model_name}", require_ready=True) origin_model = apps.get_model(f"bookwyrm.{origin_model_name}", require_ready=True)
with transaction.atomic(): if isinstance(data, str):
if isinstance(data, str): existing = model.find_existing_by_remote_id(data)
existing = model.find_existing_by_remote_id(data) if existing:
if existing: data = existing.to_activity()
data = existing.to_activity() else:
else: data = get_data(data)
data = get_data(data) activity = model.activity_serializer(**data)
activity = model.activity_serializer(**data)
# this must exist because it's the object that triggered this function # this must exist because it's the object that triggered this function
instance = origin_model.find_existing_by_remote_id(related_remote_id) instance = origin_model.find_existing_by_remote_id(related_remote_id)
if not instance: if not instance:
raise ValueError(f"Invalid related remote id: {related_remote_id}") raise ValueError(f"Invalid related remote id: {related_remote_id}")
# set the origin's remote id on the activity so it will be there when # set the origin's remote id on the activity so it will be there when
# the model instance is created # the model instance is created
# edition.parentWork = instance, for example # edition.parentWork = instance, for example
model_field = getattr(model, related_field_name) model_field = getattr(model, related_field_name)
if hasattr(model_field, "activitypub_field"): if hasattr(model_field, "activitypub_field"):
setattr( setattr(activity, getattr(model_field, "activitypub_field"), instance.remote_id)
activity, getattr(model_field, "activitypub_field"), instance.remote_id item = activity.to_model()
)
item = activity.to_model()
# if the related field isn't serialized (attachments on Status), then # if the related field isn't serialized (attachments on Status), then
# we have to set it post-creation # we have to set it post-creation
if not hasattr(model_field, "activitypub_field"): if not hasattr(model_field, "activitypub_field"):
setattr(item, related_field_name, instance) setattr(item, related_field_name, instance)
item.save() item.save()
def get_model_from_type(activity_type): def get_model_from_type(activity_type):
@ -304,3 +287,27 @@ def resolve_remote_id(
# if we're refreshing, "result" will be set and we'll update it # if we're refreshing, "result" will be set and we'll update it
return item.to_model(model=model, instance=result, save=save) return item.to_model(model=model, instance=result, save=save)
@dataclass(init=False)
class Link(ActivityObject):
"""for tagging a book in a status"""
href: str
name: str = None
mediaType: str = None
id: str = None
attributedTo: str = None
type: str = "Link"
def serialize(self, **kwargs):
"""remove fields"""
omit = ("id", "type", "@context")
return super().serialize(omit=omit)
@dataclass(init=False)
class Mention(Link):
"""a subtype of Link for mentioning an actor"""
type: str = "Mention"

View file

@ -17,6 +17,8 @@ class BookData(ActivityObject):
goodreadsKey: str = None goodreadsKey: str = None
bnfId: str = None bnfId: str = None
lastEditedBy: str = None lastEditedBy: str = None
links: List[str] = field(default_factory=lambda: [])
fileLinks: List[str] = field(default_factory=lambda: [])
# pylint: disable=invalid-name # pylint: disable=invalid-name

View file

@ -15,6 +15,11 @@ class PublicKey(ActivityObject):
publicKeyPem: str publicKeyPem: str
type: str = "PublicKey" type: str = "PublicKey"
def serialize(self, **kwargs):
"""remove fields"""
omit = ("type", "@context")
return super().serialize(omit=omit)
# pylint: disable=invalid-name # pylint: disable=invalid-name
@dataclass(init=False) @dataclass(init=False)

View file

@ -216,6 +216,18 @@ class CoverForm(CustomForm):
help_texts = {f: None for f in fields} help_texts = {f: None for f in fields}
class LinkDomainForm(CustomForm):
class Meta:
model = models.LinkDomain
fields = ["name"]
class FileLinkForm(CustomForm):
class Meta:
model = models.FileLink
fields = ["url", "filetype", "availability", "book", "added_by"]
class EditionForm(CustomForm): class EditionForm(CustomForm):
class Meta: class Meta:
model = models.Edition model = models.Edition
@ -230,6 +242,8 @@ class EditionForm(CustomForm):
"shelves", "shelves",
"connector", "connector",
"search_vector", "search_vector",
"links",
"file_links",
] ]
widgets = { widgets = {
"title": forms.TextInput(attrs={"aria-describedby": "desc_title"}), "title": forms.TextInput(attrs={"aria-describedby": "desc_title"}),
@ -439,7 +453,7 @@ class GroupForm(CustomForm):
class ReportForm(CustomForm): class ReportForm(CustomForm):
class Meta: class Meta:
model = models.Report model = models.Report
fields = ["user", "reporter", "statuses", "note"] fields = ["user", "reporter", "statuses", "links", "note"]
class EmailBlocklistForm(CustomForm): class EmailBlocklistForm(CustomForm):
@ -478,3 +492,19 @@ class SortListForm(forms.Form):
("descending", _("Descending")), ("descending", _("Descending")),
), ),
) )
class ReadThroughForm(CustomForm):
def clean(self):
"""make sure the email isn't in use by a registered user"""
cleaned_data = super().clean()
start_date = cleaned_data.get("start_date")
finish_date = cleaned_data.get("finish_date")
if start_date and finish_date and start_date > finish_date:
self.add_error(
"finish_date", _("Reading finish date cannot be before start date.")
)
class Meta:
model = models.ReadThrough
fields = ["user", "book", "start_date", "finish_date"]

View file

@ -5,7 +5,9 @@ import redis
from bookwyrm import settings from bookwyrm import settings
r = redis.Redis( r = redis.Redis(
host=settings.REDIS_ACTIVITY_HOST, port=settings.REDIS_ACTIVITY_PORT, db=0 host=settings.REDIS_ACTIVITY_HOST,
port=settings.REDIS_ACTIVITY_PORT,
db=settings.REDIS_ACTIVITY_DB_INDEX,
) )

View file

@ -3,7 +3,7 @@ from django.core.management.base import BaseCommand
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from bookwyrm.models import Connector, FederatedServer, SiteSettings, User from bookwyrm import models
def init_groups(): def init_groups():
@ -55,7 +55,7 @@ def init_permissions():
}, },
] ]
content_type = ContentType.objects.get_for_model(User) content_type = models.ContentType.objects.get_for_model(User)
for permission in permissions: for permission in permissions:
permission_obj = Permission.objects.create( permission_obj = Permission.objects.create(
codename=permission["codename"], codename=permission["codename"],
@ -72,7 +72,7 @@ def init_permissions():
def init_connectors(): def init_connectors():
"""access book data sources""" """access book data sources"""
Connector.objects.create( models.Connector.objects.create(
identifier="bookwyrm.social", identifier="bookwyrm.social",
name="BookWyrm dot Social", name="BookWyrm dot Social",
connector_file="bookwyrm_connector", connector_file="bookwyrm_connector",
@ -84,7 +84,7 @@ def init_connectors():
priority=2, priority=2,
) )
Connector.objects.create( models.Connector.objects.create(
identifier="inventaire.io", identifier="inventaire.io",
name="Inventaire", name="Inventaire",
connector_file="inventaire", connector_file="inventaire",
@ -96,7 +96,7 @@ def init_connectors():
priority=3, priority=3,
) )
Connector.objects.create( models.Connector.objects.create(
identifier="openlibrary.org", identifier="openlibrary.org",
name="OpenLibrary", name="OpenLibrary",
connector_file="openlibrary", connector_file="openlibrary",
@ -113,7 +113,7 @@ def init_federated_servers():
"""big no to nazis""" """big no to nazis"""
built_in_blocks = ["gab.ai", "gab.com"] built_in_blocks = ["gab.ai", "gab.com"]
for server in built_in_blocks: for server in built_in_blocks:
FederatedServer.objects.create( models.FederatedServer.objects.create(
server_name=server, server_name=server,
status="blocked", status="blocked",
) )
@ -121,18 +121,61 @@ def init_federated_servers():
def init_settings(): def init_settings():
"""info about the instance""" """info about the instance"""
SiteSettings.objects.create( models.SiteSettings.objects.create(
support_link="https://www.patreon.com/bookwyrm", support_link="https://www.patreon.com/bookwyrm",
support_title="Patreon", support_title="Patreon",
) )
def init_link_domains(*_):
"""safe book links"""
domains = [
("standardebooks.org", "Standard EBooks"),
("www.gutenberg.org", "Project Gutenberg"),
("archive.org", "Internet Archive"),
("openlibrary.org", "Open Library"),
("theanarchistlibrary.org", "The Anarchist Library"),
]
for domain, name in domains:
models.LinkDomain.objects.create(
domain=domain,
name=name,
status="approved",
)
class Command(BaseCommand): class Command(BaseCommand):
help = "Initializes the database with starter data" help = "Initializes the database with starter data"
def add_arguments(self, parser):
parser.add_argument(
"--limit",
default=None,
help="Limit init to specific table",
)
def handle(self, *args, **options): def handle(self, *args, **options):
init_groups() limit = options.get("limit")
init_permissions() tables = [
init_connectors() "group",
init_federated_servers() "permission",
init_settings() "connector",
"federatedserver",
"settings",
"linkdomain",
]
if limit and limit not in tables:
raise Exception("Invalid table limit:", limit)
if not limit or limit == "group":
init_groups()
if not limit or limit == "permission":
init_permissions()
if not limit or limit == "connector":
init_connectors()
if not limit or limit == "federatedserver":
init_federated_servers()
if not limit or limit == "settings":
init_settings()
if not limit or limit == "linkdomain":
init_link_domains()

View file

@ -22,13 +22,6 @@ class Command(BaseCommand):
help = "Populate list streams for all users" help = "Populate list streams for all users"
def add_arguments(self, parser):
parser.add_argument(
"--stream",
default=None,
help="Specifies which time of stream to populate",
)
# pylint: disable=no-self-use,unused-argument # pylint: disable=no-self-use,unused-argument
def handle(self, *args, **options): def handle(self, *args, **options):
"""run feed builder""" """run feed builder"""

View file

@ -0,0 +1,55 @@
# Generated by Django 3.2.10 on 2022-01-12 23:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0125_alter_user_preferred_language"),
]
operations = [
migrations.AlterField(
model_name="annualgoal",
name="privacy",
field=models.CharField(
choices=[
("public", "Public"),
("unlisted", "Unlisted"),
("followers", "Followers"),
("direct", "Private"),
],
default="public",
max_length=255,
),
),
migrations.AlterField(
model_name="importjob",
name="privacy",
field=models.CharField(
choices=[
("public", "Public"),
("unlisted", "Unlisted"),
("followers", "Followers"),
("direct", "Private"),
],
default="public",
max_length=255,
),
),
migrations.AlterField(
model_name="user",
name="default_post_privacy",
field=models.CharField(
choices=[
("public", "Public"),
("unlisted", "Unlisted"),
("followers", "Followers"),
("direct", "Private"),
],
default="public",
max_length=255,
),
),
]

View file

@ -0,0 +1,144 @@
# Generated by Django 3.2.10 on 2022-01-10 21:20
import bookwyrm.models.activitypub_mixin
import bookwyrm.models.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0125_alter_user_preferred_language"),
]
operations = [
migrations.CreateModel(
name="LinkDomain",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_date", models.DateTimeField(auto_now_add=True)),
("updated_date", models.DateTimeField(auto_now=True)),
(
"remote_id",
bookwyrm.models.fields.RemoteIdField(
max_length=255,
null=True,
validators=[bookwyrm.models.fields.validate_remote_id],
),
),
("domain", models.CharField(max_length=255, unique=True)),
(
"status",
models.CharField(
choices=[
("approved", "Approved"),
("blocked", "Blocked"),
("pending", "Pending"),
],
default="pending",
max_length=50,
),
),
("name", models.CharField(max_length=100)),
(
"reported_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="Link",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_date", models.DateTimeField(auto_now_add=True)),
("updated_date", models.DateTimeField(auto_now=True)),
(
"remote_id",
bookwyrm.models.fields.RemoteIdField(
max_length=255,
null=True,
validators=[bookwyrm.models.fields.validate_remote_id],
),
),
("url", bookwyrm.models.fields.URLField(max_length=255)),
(
"added_by",
bookwyrm.models.fields.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
(
"domain",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="links",
to="bookwyrm.linkdomain",
),
),
],
options={
"abstract": False,
},
bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model),
),
migrations.CreateModel(
name="FileLink",
fields=[
(
"link_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="bookwyrm.link",
),
),
("filetype", bookwyrm.models.fields.CharField(max_length=5)),
(
"book",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="file_links",
to="bookwyrm.book",
),
),
],
options={
"abstract": False,
},
bases=("bookwyrm.link",),
),
]

View file

@ -0,0 +1,22 @@
# Generated by Django 3.2.10 on 2022-01-10 22:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0126_filelink_link_linkdomain"),
]
operations = [
migrations.RemoveConstraint(
model_name="report",
name="self_report",
),
migrations.AddField(
model_name="report",
name="links",
field=models.ManyToManyField(blank=True, to="bookwyrm.Link"),
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.10 on 2022-01-13 01:14
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0126_auto_20220112_2315"),
("bookwyrm", "0127_auto_20220110_2211"),
]
operations = []

View file

@ -0,0 +1,32 @@
# Generated by Django 3.2.10 on 2022-01-17 17:16
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0128_merge_0126_auto_20220112_2315_0127_auto_20220110_2211"),
]
operations = [
migrations.AddField(
model_name="filelink",
name="availability",
field=bookwyrm.models.fields.CharField(
choices=[
("free", "Free"),
("purchase", "Purchasable"),
("loan", "Available for loan"),
],
default="free",
max_length=100,
),
),
migrations.AlterField(
model_name="filelink",
name="filetype",
field=bookwyrm.models.fields.CharField(max_length=50),
),
]

View file

@ -4,6 +4,7 @@ import sys
from .book import Book, Work, Edition, BookDataModel from .book import Book, Work, Edition, BookDataModel
from .author import Author from .author import Author
from .link import Link, FileLink, LinkDomain
from .connector import Connector from .connector import Connector
from .shelf import Shelf, ShelfBook from .shelf import Shelf, ShelfBook

View file

@ -241,8 +241,11 @@ class Work(OrderedCollectionPageMixin, Book):
) )
activity_serializer = activitypub.Work activity_serializer = activitypub.Work
serialize_reverse_fields = [("editions", "editions", "-edition_rank")] serialize_reverse_fields = [
deserialize_reverse_fields = [("editions", "editions")] ("editions", "editions", "-edition_rank"),
("file_links", "fileLinks", "-created_date"),
]
deserialize_reverse_fields = [("editions", "editions"), ("file_links", "fileLinks")]
# https://schema.org/BookFormatType # https://schema.org/BookFormatType
@ -296,6 +299,8 @@ class Edition(Book):
activity_serializer = activitypub.Edition activity_serializer = activitypub.Edition
name_field = "title" name_field = "title"
serialize_reverse_fields = [("file_links", "fileLinks", "-created_date")]
deserialize_reverse_fields = [("file_links", "fileLinks")]
def get_rank(self): def get_rank(self):
"""calculate how complete the data is on this edition""" """calculate how complete the data is on this edition"""
@ -337,6 +342,11 @@ class Edition(Book):
# set rank # set rank
self.edition_rank = self.get_rank() self.edition_rank = self.get_rank()
# clear author cache
if self.id:
for author_id in self.authors.values_list("id", flat=True):
cache.delete(f"author-books-{author_id}")
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
@classmethod @classmethod

View file

@ -203,9 +203,12 @@ class UsernameField(ActivitypubFieldMixin, models.CharField):
return value.split("@")[0] return value.split("@")[0]
PrivacyLevels = models.TextChoices( PrivacyLevels = [
"Privacy", ["public", "unlisted", "followers", "direct"] ("public", _("Public")),
) ("unlisted", _("Unlisted")),
("followers", _("Followers")),
("direct", _("Private")),
]
class PrivacyField(ActivitypubFieldMixin, models.CharField): class PrivacyField(ActivitypubFieldMixin, models.CharField):
@ -214,9 +217,7 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField):
public = "https://www.w3.org/ns/activitystreams#Public" public = "https://www.w3.org/ns/activitystreams#Public"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__( super().__init__(*args, max_length=255, choices=PrivacyLevels, default="public")
*args, max_length=255, choices=PrivacyLevels.choices, default="public"
)
# pylint: disable=invalid-name # pylint: disable=invalid-name
def set_field_from_activity(self, instance, data, overwrite=True): def set_field_from_activity(self, instance, data, overwrite=True):
@ -516,6 +517,10 @@ class CharField(ActivitypubFieldMixin, models.CharField):
"""activitypub-aware char field""" """activitypub-aware char field"""
class URLField(ActivitypubFieldMixin, models.URLField):
"""activitypub-aware url field"""
class TextField(ActivitypubFieldMixin, models.TextField): class TextField(ActivitypubFieldMixin, models.TextField):
"""activitypub-aware text field""" """activitypub-aware text field"""

View file

@ -40,9 +40,7 @@ class ImportJob(models.Model):
mappings = models.JSONField() mappings = models.JSONField()
complete = models.BooleanField(default=False) complete = models.BooleanField(default=False)
source = models.CharField(max_length=100) source = models.CharField(max_length=100)
privacy = models.CharField( privacy = models.CharField(max_length=255, default="public", choices=PrivacyLevels)
max_length=255, default="public", choices=PrivacyLevels.choices
)
retry = models.BooleanField(default=False) retry = models.BooleanField(default=False)
@property @property

95
bookwyrm/models/link.py Normal file
View file

@ -0,0 +1,95 @@
""" outlink data """
from urllib.parse import urlparse
from django.core.exceptions import PermissionDenied
from django.db import models
from django.utils.translation import gettext_lazy as _
from bookwyrm import activitypub
from .activitypub_mixin import ActivitypubMixin
from .base_model import BookWyrmModel
from . import fields
class Link(ActivitypubMixin, BookWyrmModel):
"""a link to a website"""
url = fields.URLField(max_length=255, activitypub_field="href")
added_by = fields.ForeignKey(
"User", on_delete=models.SET_NULL, null=True, activitypub_field="attributedTo"
)
domain = models.ForeignKey(
"LinkDomain",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="links",
)
activity_serializer = activitypub.Link
reverse_unfurl = True
@property
def name(self):
"""link name via the assocaited domain"""
return self.domain.name
def save(self, *args, **kwargs):
"""create a link"""
# get or create the associated domain
if not self.domain:
domain = urlparse(self.url).netloc
self.domain, _ = LinkDomain.objects.get_or_create(domain=domain)
# this is never broadcast, the owning model broadcasts an update
if "broadcast" in kwargs:
del kwargs["broadcast"]
return super().save(*args, **kwargs)
AvailabilityChoices = [
("free", _("Free")),
("purchase", _("Purchasable")),
("loan", _("Available for loan")),
]
class FileLink(Link):
"""a link to a file"""
book = models.ForeignKey(
"Book", on_delete=models.CASCADE, related_name="file_links", null=True
)
filetype = fields.CharField(max_length=50, activitypub_field="mediaType")
availability = fields.CharField(
max_length=100, choices=AvailabilityChoices, default="free"
)
StatusChoices = [
("approved", _("Approved")),
("blocked", _("Blocked")),
("pending", _("Pending")),
]
class LinkDomain(BookWyrmModel):
"""List of domains used in links"""
domain = models.CharField(max_length=255, unique=True)
status = models.CharField(max_length=50, choices=StatusChoices, default="pending")
name = models.CharField(max_length=100)
reported_by = models.ForeignKey(
"User", blank=True, null=True, on_delete=models.SET_NULL
)
def raise_not_editable(self, viewer):
if viewer.has_perm("moderate_post"):
return
raise PermissionDenied()
def save(self, *args, **kwargs):
"""set a default name"""
if not self.name:
self.name = self.domain
super().save(*args, **kwargs)

View file

@ -1,5 +1,6 @@
""" progress in a book """ """ progress in a book """
from django.core import validators from django.core import validators
from django.core.cache import cache
from django.db import models from django.db import models
from django.db.models import F, Q from django.db.models import F, Q
@ -30,6 +31,7 @@ class ReadThrough(BookWyrmModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""update user active time""" """update user active time"""
cache.delete(f"latest_read_through-{self.user.id}-{self.book.id}")
self.user.update_active_date() self.user.update_active_date()
# an active readthrough must have an unset finish date # an active readthrough must have an unset finish date
if self.finish_date: if self.finish_date:

View file

@ -1,7 +1,6 @@
""" defines relationships between users """ """ defines relationships between users """
from django.apps import apps from django.apps import apps
from django.core.cache import cache from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.db import models, transaction, IntegrityError from django.db import models, transaction, IntegrityError
from django.db.models import Q from django.db.models import Q
@ -41,15 +40,12 @@ class UserRelationship(BookWyrmModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""clear the template cache""" """clear the template cache"""
# invalidate the template cache # invalidate the template cache
cache_keys = [ cache.delete_many(
make_template_fragment_key( [
"follow_button", [self.user_subject.id, self.user_object.id] f"relationship-{self.user_subject.id}-{self.user_object.id}",
), f"relationship-{self.user_object.id}-{self.user_subject.id}",
make_template_fragment_key( ]
"follow_button", [self.user_object.id, self.user_subject.id] )
),
]
cache.delete_many(cache_keys)
super().save(*args, **kwargs) super().save(*args, **kwargs)
class Meta: class Meta:

View file

@ -1,6 +1,5 @@
""" flagged for moderation """ """ flagged for moderation """
from django.db import models from django.db import models
from django.db.models import F, Q
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
@ -13,14 +12,12 @@ class Report(BookWyrmModel):
note = models.TextField(null=True, blank=True) note = models.TextField(null=True, blank=True)
user = models.ForeignKey("User", on_delete=models.PROTECT) user = models.ForeignKey("User", on_delete=models.PROTECT)
statuses = models.ManyToManyField("Status", blank=True) statuses = models.ManyToManyField("Status", blank=True)
links = models.ManyToManyField("Link", blank=True)
resolved = models.BooleanField(default=False) resolved = models.BooleanField(default=False)
class Meta: class Meta:
"""don't let users report themselves""" """set order by default"""
constraints = [
models.CheckConstraint(check=~Q(reporter=F("user")), name="self_report")
]
ordering = ("-created_date",) ordering = ("-created_date",)

View file

@ -1,5 +1,6 @@
""" puttin' books on shelves """ """ puttin' books on shelves """
import re import re
from django.core.cache import cache
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
@ -94,8 +95,15 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.user: if not self.user:
self.user = self.shelf.user self.user = self.shelf.user
if self.id and self.user.local:
cache.delete(f"book-on-shelf-{self.book.id}-{self.shelf.id}")
super().save(*args, **kwargs) super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
if self.id and self.user.local:
cache.delete(f"book-on-shelf-{self.book.id}-{self.shelf.id}")
super().delete(*args, **kwargs)
class Meta: class Meta:
"""an opinionated constraint! """an opinionated constraint!
you can't put a book on shelf twice""" you can't put a book on shelf twice"""

View file

@ -90,6 +90,14 @@ class SiteSettings(models.Model):
return get_absolute_url(uploaded) return get_absolute_url(uploaded)
return urljoin(STATIC_FULL_URL, default_path) return urljoin(STATIC_FULL_URL, default_path)
def save(self, *args, **kwargs):
"""if require_confirm_email is disabled, make sure no users are pending"""
if not self.require_confirm_email:
User.objects.filter(is_active=False, deactivation_reason="pending").update(
is_active=True, deactivation_reason=None
)
super().save(*args, **kwargs)
class SiteInvite(models.Model): class SiteInvite(models.Model):
"""gives someone access to create an account on the instance""" """gives someone access to create an account on the instance"""

View file

@ -3,6 +3,7 @@ from dataclasses import MISSING
import re import re
from django.apps import apps from django.apps import apps
from django.core.cache import cache
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
@ -373,6 +374,12 @@ class Review(BookStatus):
activity_serializer = activitypub.Review activity_serializer = activitypub.Review
pure_type = "Article" pure_type = "Article"
def save(self, *args, **kwargs):
"""clear rating caches"""
if self.book.parent_work:
cache.delete(f"book-rating-{self.book.parent_work.id}-*")
super().save(*args, **kwargs)
class ReviewRating(Review): class ReviewRating(Review):
"""a subtype of review that only contains a rating""" """a subtype of review that only contains a rating"""

View file

@ -129,7 +129,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
related_name="favorite_statuses", related_name="favorite_statuses",
) )
default_post_privacy = models.CharField( default_post_privacy = models.CharField(
max_length=255, default="public", choices=fields.PrivacyLevels.choices max_length=255, default="public", choices=fields.PrivacyLevels
) )
remote_id = fields.RemoteIdField(null=True, unique=True, activitypub_field="id") remote_id = fields.RemoteIdField(null=True, unique=True, activitypub_field="id")
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
@ -346,6 +346,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
"""deactivate rather than delete a user""" """deactivate rather than delete a user"""
# pylint: disable=attribute-defined-outside-init
self.is_active = False self.is_active = False
# skip the logic in this class's save() # skip the logic in this class's save()
super().save(*args, **kwargs) super().save(*args, **kwargs)
@ -406,14 +407,6 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
self.private_key, self.public_key = create_key_pair() self.private_key, self.public_key = create_key_pair()
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
def to_activity(self, **kwargs):
"""override default AP serializer to add context object
idk if this is the best way to go about this"""
activity_object = super().to_activity(**kwargs)
del activity_object["@context"]
del activity_object["type"]
return activity_object
def get_current_year(): def get_current_year():
"""sets default year for annual goal to this year""" """sets default year for annual goal to this year"""
@ -427,7 +420,7 @@ class AnnualGoal(BookWyrmModel):
goal = models.IntegerField(validators=[MinValueValidator(1)]) goal = models.IntegerField(validators=[MinValueValidator(1)])
year = models.IntegerField(default=get_current_year) year = models.IntegerField(default=get_current_year)
privacy = models.CharField( privacy = models.CharField(
max_length=255, default="public", choices=fields.PrivacyLevels.choices max_length=255, default="public", choices=fields.PrivacyLevels
) )
class Meta: class Meta:

View file

@ -8,7 +8,7 @@ r = redis.Redis(
host=settings.REDIS_ACTIVITY_HOST, host=settings.REDIS_ACTIVITY_HOST,
port=settings.REDIS_ACTIVITY_PORT, port=settings.REDIS_ACTIVITY_PORT,
password=settings.REDIS_ACTIVITY_PASSWORD, password=settings.REDIS_ACTIVITY_PASSWORD,
db=0, db=settings.REDIS_ACTIVITY_DB_INDEX,
) )

View file

@ -9,12 +9,12 @@ from django.utils.translation import gettext_lazy as _
env = Env() env = Env()
env.read_env() env.read_env()
DOMAIN = env("DOMAIN") DOMAIN = env("DOMAIN")
VERSION = "0.1.1" VERSION = "0.2.0"
PAGE_LENGTH = env("PAGE_LENGTH", 15) PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English") DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
JS_CACHE = "2d3181e1" JS_CACHE = "76c5ff1f"
# email # email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend") EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
@ -25,7 +25,7 @@ EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", True) EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", True)
EMAIL_USE_SSL = env.bool("EMAIL_USE_SSL", False) EMAIL_USE_SSL = env.bool("EMAIL_USE_SSL", False)
EMAIL_SENDER_NAME = env("EMAIL_SENDER_NAME", "admin") EMAIL_SENDER_NAME = env("EMAIL_SENDER_NAME", "admin")
EMAIL_SENDER_DOMAIN = env("EMAIL_SENDER_NAME", DOMAIN) EMAIL_SENDER_DOMAIN = env("EMAIL_SENDER_DOMAIN", DOMAIN)
EMAIL_SENDER = f"{EMAIL_SENDER_NAME}@{EMAIL_SENDER_DOMAIN}" EMAIL_SENDER = f"{EMAIL_SENDER_NAME}@{EMAIL_SENDER_DOMAIN}"
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
@ -106,6 +106,58 @@ TEMPLATES = [
}, },
] ]
LOG_LEVEL = env("LOG_LEVEL", "INFO").upper()
# Override aspects of the default handler to our taste
# See https://docs.djangoproject.com/en/3.2/topics/logging/#default-logging-configuration
# for a reference to the defaults we're overriding
#
# It seems that in order to override anything you have to include its
# entire dependency tree (handlers and filters) which makes this a
# bit verbose
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"filters": {
# These are copied from the default configuration, required for
# implementing mail_admins below
"require_debug_false": {
"()": "django.utils.log.RequireDebugFalse",
},
"require_debug_true": {
"()": "django.utils.log.RequireDebugTrue",
},
},
"handlers": {
# Overrides the default handler to make it log to console
# regardless of the DEBUG setting (default is to not log to
# console if DEBUG=False)
"console": {
"level": LOG_LEVEL,
"class": "logging.StreamHandler",
},
# This is copied as-is from the default logger, and is
# required for the django section below
"mail_admins": {
"level": "ERROR",
"filters": ["require_debug_false"],
"class": "django.utils.log.AdminEmailHandler",
},
},
"loggers": {
# Install our new console handler for Django's logger, and
# override the log level while we're at it
"django": {
"handlers": ["console", "mail_admins"],
"level": LOG_LEVEL,
},
# Add a bookwyrm-specific logger
"bookwyrm": {
"handlers": ["console"],
"level": LOG_LEVEL,
},
},
}
WSGI_APPLICATION = "bookwyrm.wsgi.application" WSGI_APPLICATION = "bookwyrm.wsgi.application"
@ -113,6 +165,7 @@ WSGI_APPLICATION = "bookwyrm.wsgi.application"
REDIS_ACTIVITY_HOST = env("REDIS_ACTIVITY_HOST", "localhost") REDIS_ACTIVITY_HOST = env("REDIS_ACTIVITY_HOST", "localhost")
REDIS_ACTIVITY_PORT = env("REDIS_ACTIVITY_PORT", 6379) REDIS_ACTIVITY_PORT = env("REDIS_ACTIVITY_PORT", 6379)
REDIS_ACTIVITY_PASSWORD = env("REDIS_ACTIVITY_PASSWORD", None) REDIS_ACTIVITY_PASSWORD = env("REDIS_ACTIVITY_PASSWORD", None)
REDIS_ACTIVITY_DB_INDEX = env("REDIS_ACTIVITY_DB_INDEX", 0)
MAX_STREAM_LENGTH = int(env("MAX_STREAM_LENGTH", 200)) MAX_STREAM_LENGTH = int(env("MAX_STREAM_LENGTH", 200))
@ -139,7 +192,7 @@ else:
CACHES = { CACHES = {
"default": { "default": {
"BACKEND": "django_redis.cache.RedisCache", "BACKEND": "django_redis.cache.RedisCache",
"LOCATION": f"redis://:{REDIS_ACTIVITY_PASSWORD}@{REDIS_ACTIVITY_HOST}:{REDIS_ACTIVITY_PORT}/0", "LOCATION": f"redis://:{REDIS_ACTIVITY_PASSWORD}@{REDIS_ACTIVITY_HOST}:{REDIS_ACTIVITY_PORT}/{REDIS_ACTIVITY_DB_INDEX}",
"OPTIONS": { "OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient", "CLIENT_CLASS": "django_redis.client.DefaultClient",
}, },

View file

@ -720,6 +720,11 @@ ol.ordered-list li::before {
} }
} }
.overflow-wrap-anywhere {
overflow-wrap: anywhere;
min-width: 10em;
}
/* Threads /* Threads
******************************************************************************/ ******************************************************************************/
@ -751,6 +756,13 @@ ol.ordered-list li::before {
padding: 0 0.75em; padding: 0 0.75em;
} }
/* Notifications page
******************************************************************************/
.notification a.icon {
text-decoration: none !important;
}
/* Breadcrumbs /* Breadcrumbs
******************************************************************************/ ******************************************************************************/

View file

@ -0,0 +1,184 @@
(function () {
"use strict";
/**
* Suggest a completion as a user types
*
* Use `data-autocomplete="<completions set identifier>"`on the input field.
* specifying the trie to be used for autocomplete
*
* @example
* <input
* type="input"
* data-autocomplete="mimetype"
* >
* @param {Event} event
* @return {undefined}
*/
function autocomplete(event) {
const input = event.target;
// Get suggestions
let trie = tries[input.getAttribute("data-autocomplete")];
let suggestions = getSuggestions(input.value, trie);
const boxId = input.getAttribute("list");
// Create suggestion box, if needed
let suggestionsBox = document.getElementById(boxId);
// Clear existing suggestions
suggestionsBox.innerHTML = "";
// Populate suggestions box
suggestions.forEach((suggestion) => {
const suggestionItem = document.createElement("option");
suggestionItem.textContent = suggestion;
suggestionsBox.appendChild(suggestionItem);
});
}
function getSuggestions(input, trie) {
// Follow the trie through the provided input
input = input.toLowerCase();
input.split("").forEach((letter) => {
if (!trie) {
return;
}
trie = trie[letter];
});
if (!trie) {
return [];
}
return searchTrie(trie);
}
function searchTrie(trie) {
const options = Object.values(trie);
if (typeof trie == "string") {
return [trie];
}
return options
.map((option) => {
const newTrie = option;
if (typeof newTrie == "string") {
return [newTrie];
}
return searchTrie(newTrie);
})
.reduce((prev, next) => prev.concat(next));
}
document.querySelectorAll("[data-autocomplete]").forEach((input) => {
input.addEventListener("input", autocomplete);
});
})();
const tries = {
mimetype: {
a: {
a: {
c: "AAC",
},
z: {
w: "AZW",
},
},
d: {
a: {
i: {
s: {
y: "Daisy",
},
},
},
},
e: {
p: {
u: {
b: "ePub",
},
},
},
f: {
l: {
a: {
c: "FLAC",
},
},
},
h: {
t: {
m: {
l: "HTML",
},
},
},
m: {
4: {
a: "M4A",
b: "M4B",
},
o: {
b: {
i: "MOBI",
},
},
p: {
3: "MP3",
},
},
o: {
g: {
g: "OGG",
},
},
p: {
d: {
f: "PDF",
},
l: {
a: {
i: {
n: {
t: {
e: {
x: {
t: "Plaintext",
},
},
},
},
},
},
},
r: {
i: {
n: {
t: {
" ": {
b: {
o: {
o: {
k: "Print book",
},
},
},
},
},
},
},
},
},
},
};

View file

@ -35,7 +35,7 @@ let BookWyrm = new (class {
.forEach((node) => node.addEventListener("change", this.disableIfTooLarge.bind(this))); .forEach((node) => node.addEventListener("change", this.disableIfTooLarge.bind(this)));
document document
.querySelectorAll("button[data-modal-open]") .querySelectorAll("[data-modal-open]")
.forEach((node) => node.addEventListener("click", this.handleModalButton.bind(this))); .forEach((node) => node.addEventListener("click", this.handleModalButton.bind(this)));
document document

View file

@ -2,7 +2,7 @@
{% load humanize %} {% load humanize %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}
{% load bookwyrm_tags %} {% load landing_page_tags %}
{% load cache %} {% load cache %}
{% block title %} {% block title %}
@ -12,6 +12,7 @@
{% block about_content %} {% block about_content %}
{# seven day cache #} {# seven day cache #}
{% cache 604800 about_page %} {% cache 604800 about_page %}
{% get_book_superlatives as superlatives %} {% get_book_superlatives as superlatives %}
<section class="content pb-4"> <section class="content pb-4">
<h2> <h2>
@ -26,7 +27,7 @@
</p> </p>
<div class="columns"> <div class="columns">
{% if top_rated %} {% if superlatives.top_rated %}
{% with book=superlatives.top_rated.default_edition rating=top_rated.rating %} {% with book=superlatives.top_rated.default_edition rating=top_rated.rating %}
<div class="column is-one-third is-flex"> <div class="column is-one-third is-flex">
<div class="media notification"> <div class="media notification">
@ -45,7 +46,7 @@
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% if wanted %} {% if superlatives.wanted %}
{% with book=superlatives.wanted.default_edition %} {% with book=superlatives.wanted.default_edition %}
<div class="column is-one-third is-flex"> <div class="column is-one-third is-flex">
<div class="media notification"> <div class="media notification">
@ -64,7 +65,7 @@
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% if controversial %} {% if superlatives.controversial %}
{% with book=superlatives.controversial.default_edition %} {% with book=superlatives.controversial.default_edition %}
<div class="column is-one-third is-flex"> <div class="column is-one-third is-flex">
<div class="media notification"> <div class="media notification">
@ -95,7 +96,7 @@
<h2 class="title is-3">{% trans "Meet your admins" %}</h2> <h2 class="title is-3">{% trans "Meet your admins" %}</h2>
<p> <p>
{% url "conduct" as coc_path %} {% url "conduct" as coc_path %}
{% blocktrans with site_name=site.name %} {% blocktrans trimmed with site_name=site.name %}
{{ site_name }}'s moderators and administrators keep the site up and running, enforce the <a href="coc_path">code of conduct</a>, and respond when users report spam and bad behavior. {{ site_name }}'s moderators and administrators keep the site up and running, enforce the <a href="coc_path">code of conduct</a>, and respond when users report spam and bad behavior.
{% endblocktrans %} {% endblocktrans %}
</p> </p>

View file

@ -23,18 +23,13 @@
</div> </div>
</div> </div>
<div class="block columns is-flex-direction-row-reverse" itemscope itemtype="https://schema.org/Person"> <div class="block columns" itemscope itemtype="https://schema.org/Person">
<meta itemprop="name" content="{{ author.name }}"> <meta itemprop="name" content="{{ author.name }}">
{% if author.bio %}
<div class="column">
{% include "snippets/trimmed_text.html" with full=author.bio trim_length=200 %}
</div>
{% endif %}
{% firstof author.aliases author.born author.died as details %} {% firstof author.aliases author.born author.died as details %}
{% firstof author.wikipedia_link author.openlibrary_key author.inventaire_id author.isni as links %} {% firstof author.wikipedia_link author.openlibrary_key author.inventaire_id author.isni as links %}
{% if details or links %} {% if details or links %}
<div class="column is-two-fifths"> <div class="column is-3">
{% if details %} {% if details %}
<section class="block content"> <section class="block content">
<h2 class="title is-4">{% trans "Author details" %}</h2> <h2 class="title is-4">{% trans "Author details" %}</h2>
@ -71,7 +66,7 @@
<div class="box"> <div class="box">
{% if author.wikipedia_link %} {% if author.wikipedia_link %}
<div> <div>
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener" target="_blank"> <a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener noreferrer" target="_blank">
{% trans "Wikipedia" %} {% trans "Wikipedia" %}
</a> </a>
</div> </div>
@ -79,7 +74,7 @@
{% if author.isni %} {% if author.isni %}
<div class="mt-1"> <div class="mt-1">
<a itemprop="sameAs" href="{{ author.isni_link }}" rel="noopener" target="_blank"> <a itemprop="sameAs" href="{{ author.isni_link }}" rel="noopener noreferrer" target="_blank">
{% trans "View ISNI record" %} {% trans "View ISNI record" %}
</a> </a>
</div> </div>
@ -88,7 +83,7 @@
{% trans "Load data" as button_text %} {% trans "Load data" as button_text %}
{% if author.openlibrary_key %} {% if author.openlibrary_key %}
<div class="mt-1 is-flex"> <div class="mt-1 is-flex">
<a class="mr-3" itemprop="sameAs" href="{{ author.openlibrary_link }}" target="_blank" rel="noopener"> <a class="mr-3" itemprop="sameAs" href="{{ author.openlibrary_link }}" target="_blank" rel="noopener noreferrer">
{% trans "View on OpenLibrary" %} {% trans "View on OpenLibrary" %}
</a> </a>
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %} {% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
@ -103,7 +98,7 @@
{% if author.inventaire_id %} {% if author.inventaire_id %}
<div class="mt-1 is-flex"> <div class="mt-1 is-flex">
<a class="mr-3" itemprop="sameAs" href="{{ author.inventaire_link }}" target="_blank" rel="noopener"> <a class="mr-3" itemprop="sameAs" href="{{ author.inventaire_link }}" target="_blank" rel="noopener noreferrer">
{% trans "View on Inventaire" %} {% trans "View on Inventaire" %}
</a> </a>
@ -119,7 +114,7 @@
{% if author.librarything_key %} {% if author.librarything_key %}
<div class="mt-1"> <div class="mt-1">
<a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener"> <a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener noreferrer">
{% trans "View on LibraryThing" %} {% trans "View on LibraryThing" %}
</a> </a>
</div> </div>
@ -127,7 +122,7 @@
{% if author.goodreads_key %} {% if author.goodreads_key %}
<div> <div>
<a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener"> <a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener noreferrer">
{% trans "View on Goodreads" %} {% trans "View on Goodreads" %}
</a> </a>
</div> </div>
@ -137,26 +132,30 @@
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</div>
<hr aria-hidden="true"> <div class="column">
{% if author.bio %}
{% include "snippets/trimmed_text.html" with full=author.bio trim_length=200 %}
{% endif %}
<div class="block"> <h2 class="title is-4">{% blocktrans with name=author.name %}Books by {{ name }}{% endblocktrans %}</h2>
<h2 class="title is-4">{% blocktrans with name=author.name %}Books by {{ name }}{% endblocktrans %}</h2> <div class="columns is-multiline is-mobile">
<div class="columns is-multiline is-mobile"> {% for book in books %}
{% for book in books %} {% with book=book.default_edition %}
<div class="column is-one-fifth-tablet is-half-mobile is-flex is-flex-direction-column"> <div class="column is-one-fifth-tablet is-half-mobile is-flex is-flex-direction-column">
<div class="is-flex-grow-1"> <div class="is-flex-grow-1">
{% include 'landing/small-book.html' with book=book %} {% include 'landing/small-book.html' with book=book %}
</div>
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
</div> </div>
{% include 'snippets/shelve_button/shelve_button.html' with book=book %} {% endwith %}
{% endfor %}
</div>
<div>
{% include 'snippets/pagination.html' with page=books %}
</div> </div>
{% endfor %}
</div> </div>
</div> </div>
<div>
{% include 'snippets/pagination.html' with page=books %}
</div>
{% endblock %} {% endblock %}

View file

@ -1,6 +1,6 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load book_display_tags %}
{% load humanize %} {% load humanize %}
{% load utilities %} {% load utilities %}
{% load static %} {% load static %}
@ -122,7 +122,7 @@
{% trans "Load data" as button_text %} {% trans "Load data" as button_text %}
{% if book.openlibrary_key %} {% if book.openlibrary_key %}
<p> <p>
<a href="{{ book.openlibrary_link }}" target="_blank" rel="noopener"> <a href="{{ book.openlibrary_link }}" target="_blank" rel="noopener noreferrer">
{% trans "View on OpenLibrary" %} {% trans "View on OpenLibrary" %}
</a> </a>
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %} {% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
@ -136,7 +136,7 @@
{% endif %} {% endif %}
{% if book.inventaire_id %} {% if book.inventaire_id %}
<p> <p>
<a href="{{ book.inventaire_link }}" target="_blank" rel="noopener"> <a href="{{ book.inventaire_link }}" target="_blank" rel="noopener noreferrer">
{% trans "View on Inventaire" %} {% trans "View on Inventaire" %}
</a> </a>
@ -183,7 +183,7 @@
{% include 'snippets/toggle/open_button.html' with text=button_text controls_text="add_description" controls_uid=book.id focus="id_description" hide_active=True id="hide_description" %} {% include 'snippets/toggle/open_button.html' with text=button_text controls_text="add_description" controls_uid=book.id focus="id_description" hide_active=True id="hide_description" %}
<div class="box is-hidden" id="add_description_{{ book.id }}"> <div class="box is-hidden" id="add_description_{{ book.id }}">
<form name="add-description" method="POST" action="/add-description/{{ book.id }}"> <form name="add-description" method="POST" action="{% url "add-description" book.id %}">
{% csrf_token %} {% csrf_token %}
<p class="fields is-grouped"> <p class="fields is-grouped">
<label class="label" for="id_description_{{ book.id }}">{% trans "Description:" %}</label> <label class="label" for="id_description_{{ book.id }}">{% trans "Description:" %}</label>
@ -237,29 +237,21 @@
<h2 class="title is-5">{% trans "Your reading activity" %}</h2> <h2 class="title is-5">{% trans "Your reading activity" %}</h2>
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
{% trans "Add read dates" as button_text %} <button class="button is-small" data-modal-open="add-readthrough">
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="plus" class="is-small" controls_text="add_readthrough" focus="add_readthrough_focus_" %} <span class="icon icon-plus m-mobile-0" aria-hidden="true"></span>
<span class="is-sr-only-mobile">
{% trans "Add read dates" %}
</span>
</button>
</div> </div>
</header> </header>
<section class="is-hidden box" id="add_readthrough"> {% include "readthrough/readthrough_modal.html" with id="add-readthrough" %}
<form name="add-readthrough" action="/create-readthrough" method="post">
{% include 'snippets/readthrough_form.html' with readthrough=None %}
<div class="field is-grouped">
<div class="control">
<button class="button is-primary" type="submit">{% trans "Create" %}</button>
</div>
<div class="control">
{% trans "Cancel" as button_text %}
{% include 'snippets/toggle/close_button.html' with text=button_text controls_text="add_readthrough" %}
</div>
</div>
</form>
</section>
{% if not readthroughs.exists %} {% if not readthroughs.exists %}
<p>{% trans "You don't have any reading activity for this book." %}</p> <p>{% trans "You don't have any reading activity for this book." %}</p>
{% endif %} {% endif %}
{% for readthrough in readthroughs %} {% for readthrough in readthroughs %}
{% include 'book/readthrough.html' with readthrough=readthrough %} {% include 'readthrough/readthrough_list.html' with readthrough=readthrough %}
{% endfor %} {% endfor %}
</section> </section>
<hr aria-hidden="true"> <hr aria-hidden="true">
@ -327,7 +319,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="column is-one-fifth"> <div class="column is-one-fifth is-clipped">
{% if book.subjects %} {% if book.subjects %}
<section class="content block"> <section class="content block">
<h2 class="title is-5">{% trans "Subjects" %}</h2> <h2 class="title is-5">{% trans "Subjects" %}</h2>
@ -352,11 +344,11 @@
{% endif %} {% endif %}
{% if lists.exists or request.user.list_set.exists %} {% if lists.exists or request.user.list_set.exists %}
<section class="content block"> <section class="content block is-clipped">
<h2 class="title is-5">{% trans "Lists" %}</h2> <h2 class="title is-5">{% trans "Lists" %}</h2>
<ul> <ul>
{% for list in lists %} {% for list in lists %}
<li><a href="{{ list.local_path }}">{{ list.name }}</a></li> <li><a href="{{ list.local_path }}">{{ list.name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -366,7 +358,7 @@
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<label class="label" for="id_list">{% trans "Add to list" %}</label> <label class="label" for="id_list">{% trans "Add to list" %}</label>
<div class="field has-addons"> <div class="field has-addons">
<div class="select control"> <div class="select control is-clipped">
<select name="list" id="id_list"> <select name="list" id="id_list">
{% for list in user.list_set.all %} {% for list in user.list_set.all %}
<option value="{{ list.id }}">{{ list.name }}</option> <option value="{{ list.id }}">{{ list.name }}</option>
@ -381,6 +373,10 @@
{% endif %} {% endif %}
</section> </section>
{% endif %} {% endif %}
<section class="content block">
{% include "book/file_links/links.html" %}
</section>
</div> </div>
</div> </div>
@ -390,4 +386,5 @@
{% block scripts %} {% block scripts %}
<script src="{% static "js/vendor/tabs.js" %}?v={{ js_cache }}"></script> <script src="{% static "js/vendor/tabs.js" %}?v={{ js_cache }}"></script>
<script src="{% static "js/autocomplete.js" %}?v={{ js_cache }}"></script>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,64 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% load static %}
{% block modal-title %}
{% trans "Add file link" %}
{% endblock %}
{% block modal-form-open %}
<form name="add-link" method="POST" action="{% url 'file-link-add' book.id %}">
{% endblock %}
{% block modal-body %}
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="added_by" value="{{ request.user.id }}">
<p class="notification">
{% trans "Links from unknown domains will need to be approved by a moderator before they are added." %}
</p>
<div class="columns">
<div class="column is-four-fifths">
<label class="label" for="id_url">{% trans "URL:" %}</label>
<input type="url" name="url" maxlength="255" class="input" required="" id="id_url" value="{% firstof file_link_form.url.value "" %}" placeholder="https://..." aria-describedby="desc_name">
{% include 'snippets/form_errors.html' with errors_list=file_link_form.url.errors id="desc_url" %}
</div>
<div class="column is-one-fifth">
<label class="label" for="id_filetype">{% trans "File type:" %}</label>
<input
type="text"
name="filetype"
maxlength="50"
class="input"
required=""
id="id_filetype"
value="{% firstof file_link_form.filetype.value '' %}"
placeholder="ePub"
list="mimetypes-list"
data-autocomplete="mimetype"
>
<datalist id="mimetypes-list"></datalist>
{% include 'snippets/form_errors.html' with errors_list=file_link_form.filetype.errors id="desc_filetype" %}
</div>
</div>
<div>
<label class="label" for="id_availability">
{% trans "Availability:" %}
</label>
<div class="select">
{{ file_link_form.availability }}
</div>
</div>
{% endblock %}
{% block modal-footer %}
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
{% if not static %}
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% endif %}
{% endblock %}
{% block modal-form-close %}</form>{% endblock %}

View file

@ -0,0 +1,114 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load utilities %}
{% block title %}{% trans "Edit links" %}{% endblock %}
{% block content %}
<header class="block content">
<h1 class="title">
{% blocktrans with title=book|book_title %}
Links for "<em>{{ title }}</em>"
{% endblocktrans %}
</h1>
</header>
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul>
<li><a href="{% url 'book' book.id %}">{{ book|book_title }}</a></li>
<li class="is-active">
<a href="#" aria-current="page">
{% trans "Edit links" %}
</a>
</li>
</ul>
</nav>
<section class="block content">
<div class="table-container">
<table class="is-striped is-fullwidth">
<tr>
<th>{% trans "URL" %}</th>
<th>{% trans "Added by" %}</th>
<th>{% trans "Filetype" %}</th>
<th>{% trans "Domain" %}</th>
<th>{% trans "Status" %}</th>
<th colspan="2">{% trans "Actions" %}</th>
</tr>
{% for link in links %}
<tr>
<td class="overflow-wrap-anywhere">
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer">{{ link.url }}</a>
</td>
<td>
<a href="{% url 'user-feed' link.added_by.id %}">{{ link.added_by.display_name }}</a>
</td>
<td>
{{ link.filelink.filetype }}
</td>
<td>
{{ link.domain.name }}
<p>
<a href="{% url 'report-link' link.added_by.id link.id %}">{% trans "Report spam" %}</a>
</p>
</td>
<td>
{% with status=link.domain.status %}
<span class="tag {% if status == 'blocked' %}has-background-danger{% elif status == 'approved' %}has-background-primary{% endif %}">
<span class="icon" aria-hidden="true">
<span class="icon-{% if status == 'blocked' %}x{% elif status == 'approved' %}check{% else %}lock{% endif %}"></span>
</span>
<span>
{{ link.domain.get_status_display }}
</span>
</span>
{% endwith %}
</td>
<td>
<form name="edit-link" class="control" method="post" action="{% url 'file-link' book.id link.id %}">
{% csrf_token %}
<input type="hidden" name="url" value="{{ link.form.url.value }}">
<input type="hidden" name="filetype" value="{{ link.form.filetype.value }}">
<input type="hidden" name="added_by" value="{{ link.form.added_by.value }}">
<input type="hidden" name="book" value="{{ link.form.book.value }}">
<div class="field has-addons">
<div class="control">
<div class="select">
{{ link.form.availability }}
</div>
</div>
<div class="control">
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
</div>
</div>
</form>
</td>
<td>
<form name="delete-link-{{ link.id }}" class="control" method="post" action="{% url 'file-link-delete' book.id link.id %}">
{% csrf_token %}
<button class="button is-danger is-light" type="submit">Delete link</button>
</form>
</td>
</tr>
{% endfor %}
{% if not book.file_links.exists %}
<tr>
<td colspan="5"><em>{% trans "No links available for this book." %}</em></td>
</tr>
{% endif %}
</table>
</div>
{% url 'file-link-add' book.id as fallback_url %}
<form name="add-link" method="get" action="{{ fallback_url }}">
<button class="button" type="submit" data-modal-open="add-links">
<span class="icon icon-plus m-0-mobile" aria-hidden="true"></span>
<span class="is-sr-only-mobile">
{% trans "Add link to file" %}
</span>
</button>
</form>
</section>
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "File Links" %}
{% endblock %}
{% block content %}
{% include "book/file_links/add_link_modal.html" with book=book active=True static=True id="file-link" %}
{% endblock %}
{% block scripts %}
<script src="{% static "js/autocomplete.js" %}?v={{ js_cache }}"></script>
{% endblock %}

View file

@ -0,0 +1,58 @@
{% load i18n %}
{% load book_display_tags %}
{% load utilities %}
{% get_book_file_links book as links %}
{% if links.exists or request.user.is_authenticated %}
<header class="columns is-mobile mb-0">
<div class="column">
<h2 class="title is-5">{% trans "Get a copy" %}</h2>
</div>
{% if can_edit_book %}
<div class="column is-narrow">
{% url 'file-link-add' book.id as fallback_url %}
<form name="add-link" method="get" action="{{ fallback_url }}">
<button class="button is-small" type="submit" data-modal-open="add-links">
<span class="icon icon-plus">
<span class="is-sr-only">
{% trans "Add link to file" %}
</span>
</span>
</button>
</form>
</div>
{% endif %}
</header>
{% if links %}
<ul class="mt-0">
{% for link in links.all %}
{% join "verify" link.id as verify_modal %}
<li>
<a href="{{ link.url }}" rel="noopener noreferrer" target="_blank" title="{{ link.url }}" data-modal-open="{{ verify_modal }}">{{ link.name }}</a>
({{ link.filetype }})
{% if link.availability != "free" %}
<p class="help">
{{ link.get_availability_display }}
</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% for link in links.all %}
{% join "verify" link.id as verify_modal %}
{% include "book/file_links/verification_modal.html" with id=verify_modal %}
{% endfor %}
{% else %}
<em>{% trans "No links available" %}</em>
{% endif %}
{% if can_edit_book and links.exists %}
<a href="{% url 'file-link' book.id %}" class="is-pulled-right">
<span class="icon icon-pencil" aria-hidden="true"></span>
<span>{% trans "Edit links" %}</span>
</a>
{% endif %}
{% include 'book/file_links/add_link_modal.html' with book=book id="add-links" %}
{% endif %}

View file

@ -0,0 +1,29 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% block modal-title %}
{% trans "Leaving BookWyrm" %}
{% endblock %}
{% block modal-body %}
{% blocktrans trimmed with link_url=link.url %}
This link is taking you to: <code>{{ link_url }}</code>.<br>
Is that where you'd like to go?
{% endblocktrans %}
{% endblock %}
{% block modal-footer %}
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer" class="button is-primary">{% trans "Continue" %}</a>
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% if request.user.is_authenticated %}
<div class="has-text-right is-flex-grow-1">
<a href="{% url 'report-link' link.added_by.id link.id %}">{% trans "Report spam" %}</a>
</div>
{% endif %}
{% endblock %}

View file

@ -1,4 +1,4 @@
{% load bookwyrm_tags %} {% load rating_tags %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}
{% load status_display %} {% load status_display %}

View file

@ -1,4 +1,4 @@
{% load bookwyrm_tags %} {% load landing_page_tags %}
{% load utilities %} {% load utilities %}
{% load i18n %} {% load i18n %}
{% load status_display %} {% load status_display %}

View file

@ -1,6 +1,6 @@
{% extends 'feed/layout.html' %} {% extends 'feed/layout.html' %}
{% load feed_page_tags %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %}
{% block opengraph_images %} {% block opengraph_images %}

View file

@ -1,5 +1,5 @@
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load feed_page_tags %}
{% suggested_books as suggested_books %} {% suggested_books as suggested_books %}
<section class="block"> <section class="block">
@ -16,10 +16,7 @@
{% with shelf_counter=forloop.counter %} {% with shelf_counter=forloop.counter %}
<li> <li>
<p> <p>
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %} {% include "snippets/translated_shelf_name.html" with shelf=shelf %}
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
{% else %}{{ shelf.name }}{% endif %}
</p> </p>
<div class="tabs is-small is-toggle"> <div class="tabs is-small is-toggle">
<ul> <ul>

View file

@ -1,7 +1,6 @@
{% extends 'groups/layout.html' %} {% extends 'groups/layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load group_tags %}
{% load bookwyrm_group_tags %}
{% load markdown %} {% load markdown %}
{% block panel %} {% block panel %}

View file

@ -1,6 +1,6 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_group_tags %} {% load group_tags %}
{% block title %}{{ group.name }}{% endblock %} {% block title %}{{ group.name }}{% endblock %}

View file

@ -1,8 +1,7 @@
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}
{% load humanize %} {% load humanize %}
{% load bookwyrm_tags %} {% load group_tags %}
{% load bookwyrm_group_tags %}
<h2 class="title is-5">Group Members</h2> <h2 class="title is-5">Group Members</h2>
{% if group.user == request.user %} {% if group.user == request.user %}

View file

@ -3,6 +3,6 @@
{% block tooltip_content %} {% block tooltip_content %}
{% trans 'You can download your Goodreads data from the <a href="https://www.goodreads.com/review/import" target="_blank" rel="noopener">Import/Export page</a> of your Goodreads account.' %} {% trans 'You can download your Goodreads data from the <a href="https://www.goodreads.com/review/import" target="_blank" rel="noopener noreferrer">Import/Export page</a> of your Goodreads account.' %}
{% endblock %} {% endblock %}

View file

@ -1,13 +1,17 @@
{% extends 'landing/layout.html' %} {% extends 'landing/layout.html' %}
{% load i18n %} {% load i18n %}
{% load cache %} {% load cache %}
{% load landing_page_tags %}
{% block panel %} {% block panel %}
<div class="block is-hidden-tablet"> <div class="block is-hidden-tablet">
<h2 class="title has-text-centered">{% trans "Recent Books" %}</h2> <h2 class="title has-text-centered">{% trans "Recent Books" %}</h2>
</div> </div>
{% cache 60 * 60 %} {% get_current_language as LANGUAGE_CODE %}
{% cache 60 * 60 LANGUAGE_CODE %}
{% get_landing_books as books %}
<section class="tile is-ancestor"> <section class="tile is-ancestor">
<div class="tile is-vertical is-6"> <div class="tile is-vertical is-6">
<div class="tile is-parent"> <div class="tile is-parent">

View file

@ -1,4 +1,5 @@
{% load bookwyrm_tags %} {% load book_display_tags %}
{% load rating_tags %}
{% load markdown %} {% load markdown %}
{% load i18n %} {% load i18n %}

View file

@ -1,4 +1,4 @@
{% load bookwyrm_tags %} {% load rating_tags %}
{% load i18n %} {% load i18n %}
{% if book %} {% if book %}

View file

@ -1,7 +1,8 @@
{% extends 'embed-layout.html' %} {% extends 'embed-layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load book_display_tags %}
{% load bookwyrm_group_tags %} {% load rating_tags %}
{% load group_tags %}
{% load markdown %} {% load markdown %}
{% block title %}{% blocktrans with list_name=list.name owner=list.user.display_name %}{{ list_name }}, a list by {{owner}}{% endblocktrans %}{% endblock title %} {% block title %}{% blocktrans with list_name=list.name owner=list.user.display_name %}{{ list_name }}, a list by {{owner}}{% endblocktrans %}{% endblock title %}

View file

@ -1,7 +1,8 @@
{% extends 'lists/layout.html' %} {% extends 'lists/layout.html' %}
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load rating_tags %}
{% load bookwyrm_group_tags %} {% load book_display_tags %}
{% load group_tags %}
{% load markdown %} {% load markdown %}
{% block breadcrumbs %} {% block breadcrumbs %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% block primary_link %}{% spaceless %} {% block primary_link %}{% spaceless %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,9 +1,9 @@
{% load bookwyrm_tags %} {% load notification_page_tags %}
{% related_status notification as related_status %} {% related_status notification as related_status %}
<div class="box is-shadowless has-background-white-ter {% if notification.id in unread %} is-primary{% endif %}"> <div class="notification {% if notification.id in unread %}has-background-primary{% endif %}">
<div class="columns is-mobile"> <div class="columns is-mobile {% if notification.id in unread %}has-text-white{% else %}has-text-grey{% endif %}">
<div class="column is-narrow is-size-3 {% if notification.id in unread%}has-text-white{% else %}has-text-grey{% endif %}"> <div class="column is-narrow is-size-3">
<a class="has-text-dark" href="{% block primary_link %}{% endblock %}"> <a class="icon" href="{% block primary_link %}{% endblock %}">
{% block icon %}{% endblock %} {% block icon %}{% endblock %}
</a> </a>
</div> </div>

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}

View file

@ -1,4 +1,4 @@
{% extends 'notifications/items/item_layout.html' %} {% extends 'notifications/items/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}

View file

@ -3,7 +3,7 @@
xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns="http://a9.com/-/spec/opensearch/1.1/"
xmlns:moz="http://www.mozilla.org/2006/browser/search/" xmlns:moz="http://www.mozilla.org/2006/browser/search/"
> >
<ShortName>BW</ShortName> <ShortName>{{ site_name }}</ShortName>
<Description>{% blocktrans trimmed with site_name=site.name %} <Description>{% blocktrans trimmed with site_name=site.name %}
{{ site_name }} search {{ site_name }} search
{% endblocktrans %}</Description> {% endblocktrans %}</Description>

View file

@ -34,26 +34,26 @@
<div class="column"> <div class="column">
{{ form.avatar }} {{ form.avatar }}
{% include 'snippets/form_errors.html' with errors_list=form.avatar.errors id="desc_avatar" %} {% include 'snippets/form_errors.html' with errors_list=form.avatar.errors id="desc_avatar" %}
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="id_name">{% trans "Display name:" %}</label> <label class="label" for="id_name">{% trans "Display name:" %}</label>
{{ form.name }} {{ form.name }}
{% include 'snippets/form_errors.html' with errors_list=form.name.errors id="desc_name" %} {% include 'snippets/form_errors.html' with errors_list=form.name.errors id="desc_name" %}
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="id_summary">{% trans "Summary:" %}</label> <label class="label" for="id_summary">{% trans "Summary:" %}</label>
{{ form.summary }} {{ form.summary }}
{% include 'snippets/form_errors.html' with errors_list=form.summary.errors id="desc_summary" %} {% include 'snippets/form_errors.html' with errors_list=form.summary.errors id="desc_summary" %}
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="id_email">{% trans "Email address:" %}</label> <label class="label" for="id_email">{% trans "Email address:" %}</label>
{{ form.email }} {{ form.email }}
{% include 'snippets/form_errors.html' with errors_list=form.email.errors id="desc_email" %} {% include 'snippets/form_errors.html' with errors_list=form.email.errors id="desc_email" %}
</div> </div>
</div> </div>
</section> </section>

View file

@ -0,0 +1,15 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load utilities %}
{% block title %}
{% blocktrans trimmed with title=book|book_title %}
Update read dates for "<em>{{ title }}</em>"
{% endblocktrans %}
{% endblock %}
{% block content %}
{% include "readthrough/readthrough_modal.html" with book=book active=True static=True %}
{% endblock %}

View file

@ -4,6 +4,7 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}"> <input type="hidden" name="id" value="{{ readthrough.id }}">
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="user" value="{{ request.user.id }}">
<div class="field"> <div class="field">
<label class="label" tabindex="0" id="add_readthrough_focus_{{ readthrough.id }}" for="id_start_date_{{ readthrough.id }}"> <label class="label" tabindex="0" id="add_readthrough_focus_{{ readthrough.id }}" for="id_start_date_{{ readthrough.id }}">
{% trans "Started reading" %} {% trans "Started reading" %}

View file

@ -3,7 +3,7 @@
{% load tz %} {% load tz %}
{% load utilities %} {% load utilities %}
<div class="content"> <div class="content">
<div id="hide_edit_readthrough_{{ readthrough.id }}" class="box is-shadowless has-background-white-bis"> <div class="box is-shadowless has-background-white-bis">
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
{% trans "Progress Updates:" %} {% trans "Progress Updates:" %}
@ -58,7 +58,11 @@
<div class="field has-addons"> <div class="field has-addons">
<div class="control"> <div class="control">
{% trans "Edit read dates" as button_text %} {% trans "Edit read dates" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with class="is-small" text=button_text icon="pencil" controls_text="edit_readthrough" controls_uid=readthrough.id focus="edit_readthrough" %} <button class="button is-small" type="button" data-modal-open="edit_readthrough_{{ readthrough.id }}">
<span class="icon icon-pencil" title="{{ button_text }}">
<span class="is-sr-only">{{ button_text }}</span>
</span>
</button>
</div> </div>
<div class="control"> <div class="control">
{% trans "Delete these read dates" as button_text %} {% trans "Delete these read dates" as button_text %}
@ -74,16 +78,7 @@
</div> </div>
</div> </div>
<div class="box is-hidden" id="edit_readthrough_{{ readthrough.id }}" tabindex="0"> {% join "edit_readthrough" readthrough.id as edit_modal_id %}
<h3 class="title is-5">{% trans "Edit read dates" %}</h3> {% include "readthrough/readthrough_modal.html" with readthrough=readthrough id=edit_modal_id %}
<form name="edit-readthrough" action="/edit-readthrough" method="post"> {% join "delete_readthrough" readthrough.id as delete_modal_id %}
{% include 'snippets/readthrough_form.html' with readthrough=readthrough %} {% include 'readthrough/delete_readthrough_modal.html' with id=delete_modal_id %}
<div class="field is-grouped">
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
{% trans "Cancel" as button_text %}
{% include 'snippets/toggle/close_button.html' with text=button_text controls_text="edit_readthrough" controls_uid=readthrough.id %}
</div>
</form>
</div>
{% join "delete_readthrough" readthrough.id as modal_id %}
{% include 'book/delete_readthrough_modal.html' with id=modal_id %}

View file

@ -0,0 +1,80 @@
{% extends "components/modal.html" %}
{% load i18n %}
{% load utilities %}
{% block modal-title %}
{% if readthrough %}
{% blocktrans trimmed with title=book|book_title %}
Update read dates for "<em>{{ title }}</em>"
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with title=book|book_title %}
Add read dates for "<em>{{ title }}</em>"
{% endblocktrans %}
{% endif %}
{% endblock %}
{% block modal-form-open %}
<form name="add-readthrough-{{ readthrough.id }}" action="/create-readthrough" method="post">
{% endblock %}
{% block modal-body %}
{% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}">
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="user" value="{{ request.user.id }}">
<div class="field">
<label class="label" tabindex="0" id="add_readthrough_focus_{{ readthrough.id }}" for="id_start_date_{{ readthrough.id }}">
{% trans "Started reading" %}
</label>
{% firstof form.start_date.value readthrough.start_date|date:"Y-m-d" as value %}
<input
type="date"
name="start_date"
class="input"
id="id_start_date_{{ readthrough.id }}"
value="{{ value }}"
aria-describedby="desc_start_date"
>
{% include 'snippets/form_errors.html' with errors_list=form.start_date.errors id="desc_start_date" %}
</div>
{# Only show progress for editing existing readthroughs #}
{% if readthrough.id and not readthrough.finish_date %}
{% join "id_progress" readthrough.id as field_id %}
<label class="label" for="{{ field_id }}">
{% trans "Progress" %}
</label>
{% include "snippets/progress_field.html" with id=field_id %}
{% endif %}
<div class="field">
<label class="label" for="id_finish_date_{{ readthrough.id }}">
{% trans "Finished reading" %}
</label>
{% firstof form.finish_date.value readthrough.finish_date|date:"Y-m-d" as value %}
<input
type="date"
name="finish_date"
class="input"
id="id_finish_date_{{ readthrough.id }}"
value="{{ value }}"
aria-describedby="desc_finish_date"
>
{% include 'snippets/form_errors.html' with errors_list=form.finish_date.errors id="desc_finish_date" %}
</div>
{% endblock %}
{% block modal-footer %}
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
{% if not static %}
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% endif %}
{% endblock %}
{% block modal-form-close %}
</form>
{% endblock %}

View file

@ -0,0 +1,10 @@
{% extends "layout.html" %}
{% load i18n %}
{% block title %}
{% trans "Report" %}
{% endblock %}
{% block content %}
{% include "snippets/report_modal.html" with user=user active=True static=True %}
{% endblock %}

View file

@ -63,7 +63,7 @@
<strong> <strong>
<a <a
href="{{ result.view_link|default:result.key }}" href="{{ result.view_link|default:result.key }}"
rel="noopener" rel="noopener noreferrer"
target="_blank" target="_blank"
>{{ result.title }}</a> >{{ result.title }}</a>
</strong> </strong>
@ -75,9 +75,11 @@
<form class="mt-1" action="/resolve-book" method="post"> <form class="mt-1" action="/resolve-book" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="remote_id" value="{{ result.key }}"> <input type="hidden" name="remote_id" value="{{ result.key }}">
<button type="submit" class="button is-small is-link"> <div class="control">
{% trans "Import book" %} <button type="submit" class="button is-small is-link">
</button> {% trans "Import book" %}
</button>
</div>
</form> </form>
</div> </div>
</div> </div>

View file

@ -48,6 +48,17 @@
</a> </a>
</div> </div>
{% endif %} {% endif %}
{% if pending_domains %}
<div class="column">
<a href="{% url 'settings-link-domain' %}" class="notification is-primary is-block">
{% blocktrans trimmed count counter=pending_domains with display_count=pending_domains|intcomma %}
{{ display_count }} domain needs review
{% plural %}
{{ display_count }} domains need review
{% endblocktrans %}
</a>
</div>
{% endif %}
{% if not site.allow_registration and site.allow_invite_requests and invite_requests %} {% if not site.allow_registration and site.allow_invite_requests and invite_requests %}
<div class="column"> <div class="column">
<a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success is-light"> <a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success is-light">

View file

@ -47,7 +47,7 @@
<div class="field"> <div class="field">
<label class="label" for="id_file">JSON data:</label> <label class="label" for="id_file">JSON data:</label>
<aside class="help"> <aside class="help">
Expects a json file in the format provided by <a href="https://fediblock.org/" target="_blank" rel="noopener">FediBlock</a>, with a list of entries that have <code>instance</code> and <code>url</code> fields. For example: Expects a json file in the format provided by <a href="https://fediblock.org/" target="_blank" rel="noopener noreferrer">FediBlock</a>, with a list of entries that have <code>instance</code> and <code>url</code> fields. For example:
<pre> <pre>
[ [
{ {

View file

@ -62,6 +62,10 @@
{% url 'settings-ip-blocks' as url %} {% url 'settings-ip-blocks' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "IP Address Blocklist" %}</a> <a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "IP Address Blocklist" %}</a>
</li> </li>
<li>
{% url 'settings-link-domain' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Link Domains" %}</a>
</li>
</ul> </ul>
{% endif %} {% endif %}
{% if perms.bookwyrm.edit_instance_settings %} {% if perms.bookwyrm.edit_instance_settings %}

View file

@ -0,0 +1,25 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% block modal-title %}
{% blocktrans with url=domain.domain %}Set display name for {{ url }}{% endblocktrans %}
{% endblock %}
{% block modal-form-open %}
<form name="edit-domain-{{ domain.id }}" method="post" action="{% url 'settings-link-domain' status=status domain_id=domain.id %}">
{% endblock %}
{% block modal-body %}
{% csrf_token %}
<label class="label" for="id_name">{% trans "Name:" %}</label>
<div class="control">
<input type="text" id="id_name" class="input" name="name" value="{{ domain.name }}" required>
</div>
{% endblock %}
{% block modal-footer %}
<button type="submit" class="button is-primary">{% trans "Set" %}</button>
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
{% endblock %}
{% block modal-form-close %}</form>{% endblock %}

View file

@ -0,0 +1,106 @@
{% extends 'settings/layout.html' %}
{% load i18n %}
{% load utilities %}
{% block title %}{% trans "Link Domains" %}{% endblock %}
{% block header %}{% trans "Link Domains" %}{% endblock %}
{% block panel %}
<p class="notification block">
{% trans "Link domains must be approved before they are shown on book pages. Please make sure that the domains are not hosting spam, malicious code, or deceptive links before approving." %}
</p>
<div class="block">
<div class="tabs">
<ul>
{% url 'settings-link-domain' status='pending' as url %}
<li {% if request.path in url %}class="is-active" aria-current="page"{% endif %}>
<a href="{{ url }}">{% trans "Pending" %} ({{ counts.pending }})</a>
</li>
{% url 'settings-link-domain' status='approved' as url %}
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
<a href="{{ url }}">{% trans "Approved" %} ({{ counts.approved }})</a>
</li>
{% url 'settings-link-domain' status='blocked' as url %}
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
<a href="{{ url }}">{% trans "Blocked" %} ({{ counts.blocked }})</a>
</li>
</ul>
</div>
{% for domain in domains %}
{% join "domain" domain.id as domain_modal %}
<div class="box content" id="{{ domain.id }}">
<div class="columns is-mobile">
<header class="column">
<h2 class="title is-5">
{{ domain.name }}
(<a href="http://{{ domain.domain }}" target="_blank" rel="noopener noreferrer">{{ domain.domain }}</a>)
</h2>
</header>
<div class="column is-narrow">
<button type="button" class="button" data-modal-open="{{ domain_modal }}">
<span class="icon icon-pencil m-0-mobile" aria-hidden="treu"></span>
<span class="is-sr-only-mobile">{% trans "Set display name" %}</span>
</button>
</div>
</div>
<div class="block">
<details class="details-panel">
<summary>
<span role="heading" aria-level="3" class="title is-6 mb-0">
{% trans "View links" %}
({{ domain.links.count }})
</span>
<span class="details-close icon icon-x" aria-hidden></span>
</summary>
<div class="table-container mt-4">
{% include "settings/link_domains/link_table.html" with links=domain.links.all|slice:10 %}
</div>
</details>
</div>
{% include "settings/link_domains/edit_domain_modal.html" with domain=domain id=domain_modal %}
<div class="field has-addons">
{% if status != "approved" %}
<form
name="domain-{{ domains.id }}-approve"
class="control"
method="post"
action="{% url 'settings-link-domain-status' domain.id 'approved' %}"
>
{% csrf_token %}
<button type="submit" class="button is-success is-light">{% trans "Approve" %}</button>
</form>
{% endif %}
{% if status != "blocked" %}
<form
name="domain-{{ domains.id }}-block"
class="control"
method="post"
action="{% url 'settings-link-domain-status' domain.id 'blocked' %}"
>
{% csrf_token %}
<button type="submit" class="button is-danger is-light">{% trans "Block" %}</button>
</form>
{% endif %}
</div>
</div>
{% endfor %}
{% if not domains.exists %}
{% if status == "approved" %}
<em>{% trans "No domains currently approved" %}</em>
{% elif status == "pending" %}
<em>{% trans "No domains currently pending" %}</em>
{% else %}
<em>{% trans "No domains currently blocked" %}</em>
{% endif %}
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,42 @@
{% load i18n %}
{% load utilities %}
<table class="is-striped is-fullwidth">
<tr>
<th>{% trans "URL" %}</th>
<th>{% trans "Added by" %}</th>
<th>{% trans "Filetype" %}</th>
<th>{% trans "Book" %}</th>
{% block additional_headers %}{% endblock %}
</tr>
{% for link in links %}
<tr>
<td class="overflow-wrap-anywhere">
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer">{{ link.url }}</a>
</td>
<td>
<a href="{% url 'settings-user' link.added_by.id %}">@{{ link.added_by|username }}</a>
</td>
<td>
{% if link.filelink.filetype %}
{{ link.filelink.filetype }}
{% endif %}
</td>
<td>
{% if link.filelink.book %}
{% with book=link.filelink.book %}
{% include "snippets/book_titleby.html" with book=book %}
{% endwith %}
{% endif %}
</td>
{% block additional_data %}{% endblock %}
</tr>
{% endfor %}
{% if not links %}
<tr>
<td colspan="7"><em>{% trans "No links available for this domain." %}</em></td>
</tr>
{% endif %}
</table>

View file

@ -2,10 +2,12 @@
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
{% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %} {% block title %}
{% include "settings/reports/report_header.html" with report=report %}
{% endblock %}
{% block header %} {% block header %}
{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %} {% include "settings/reports/report_header.html" with report=report %}
<a href="{% url 'settings-reports' %}" class="has-text-weight-normal help">{% trans "Back to reports" %}</a> <a href="{% url 'settings-reports' %}" class="has-text-weight-normal help">{% trans "Back to reports" %}</a>
{% endblock %} {% endblock %}
@ -15,6 +17,36 @@
{% include 'settings/reports/report_preview.html' with report=report %} {% include 'settings/reports/report_preview.html' with report=report %}
</div> </div>
{% if report.statuses.exists %}
<div class="block">
<h3 class="title is-4">{% trans "Reported statuses" %}</h3>
<ul>
{% for status in report.statuses.select_subclasses.all %}
<li>
{% if status.deleted %}
<em>{% trans "Status has been deleted" %}</em>
{% else %}
{% include 'snippets/status/status.html' with status=status moderation_mode=True %}
{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if report.links.exists %}
<div class="block">
<h3 class="title is-4">{% trans "Reported links" %}</h3>
<div class="card block">
<div class="card-content content">
<div class="table-container">
{% include "settings/reports/report_links_table.html" with links=report.links.all %}
</div>
</div>
</div>
</div>
{% endif %}
{% include 'settings/users/user_info.html' with user=report.user %} {% include 'settings/users/user_info.html' with user=report.user %}
{% include 'settings/users/user_moderation_actions.html' with user=report.user %} {% include 'settings/users/user_moderation_actions.html' with user=report.user %}
@ -41,23 +73,4 @@
<button class="button">{% trans "Comment" %}</button> <button class="button">{% trans "Comment" %}</button>
</form> </form>
</div> </div>
<div class="block">
<h3 class="title is-4">{% trans "Reported statuses" %}</h3>
{% if not report.statuses.exists %}
<em>{% trans "No statuses reported" %}</em>
{% else %}
<ul>
{% for status in report.statuses.select_subclasses.all %}
<li>
{% if status.deleted %}
<em>{% trans "Status has been deleted" %}</em>
{% else %}
{% include 'snippets/status/status.html' with status=status moderation_mode=True %}
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,22 @@
{% load i18n %}
{% load utilities %}
{% if report.statuses.exists %}
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
Report #{{ report_id }}: Status posted by @{{ username }}
{% endblocktrans %}
{% elif report.links.exists %}
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
Report #{{ report_id }}: Link added by @{{ username }}
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with report_id=report.id username=report.user|username %}
Report #{{ report_id }}: User @{{ username }}
{% endblocktrans %}
{% endif %}

View file

@ -0,0 +1,20 @@
{% extends "settings/link_domains/link_table.html" %}
{% load i18n %}
{% block additional_headers %}
<th>{% trans "Domain" %}</th>
<th>{% trans "Actions" %}</th>
{% endblock %}
{% block additional_data %}
<td>
<a href="{% url 'settings-link-domain' 'pending' %}#{{ link.domain.id }}">
{{ link.domain.domain }}
</a>
</td>
<td>
<form>
<button type="submit" class="button is-danger is-light">{% trans "Block domain" %}</button>
</form>
</td>
{% endblock %}

View file

@ -1,9 +1,13 @@
{% extends 'components/card.html' %} {% extends 'components/card.html' %}
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
{% load utilities %}
{% block card-header %} {% block card-header %}
<h2 class="card-header-title has-background-white-ter is-block"> <h2 class="card-header-title has-background-white-ter is-block">
<a href="{% url 'settings-report' report.id %}">{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}</a> <a href="{% url 'settings-report' report.id %}">
{% include "settings/reports/report_header.html" with report=report %}
</a>
</h2> </h2>
{% endblock %} {% endblock %}
@ -17,7 +21,7 @@
{% block card-footer %} {% block card-footer %}
<div class="card-footer-item"> <div class="card-footer-item">
<p>{% blocktrans with username=report.reporter.display_name path=report.reporter.local_path %}Reported by <a href="{{ path }}">{{ username }}</a>{% endblocktrans %}</p> <p>{% blocktrans with username=report.reporter|username path=report.reporter.local_path %}Reported by <a href="{{ path }}">@{{ username }}</a>{% endblocktrans %}</p>
</div> </div>
<div class="card-footer-item"> <div class="card-footer-item">
{{ report.created_date | naturaltime }} {{ report.created_date | naturaltime }}

View file

@ -5,71 +5,73 @@
{% trans "Permanently deleted" %} {% trans "Permanently deleted" %}
</div> </div>
{% else %} {% else %}
<h3>{% trans "Actions" %}</h3> <h3>{% trans "User Actions" %}</h3>
<div class="is-flex"> <div class="box">
{% if user.is_active %} <div class="is-flex">
<p class="mr-1"> {% if user.is_active %}
<a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a> <p class="mr-1">
</p> <a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a>
{% endif %} </p>
{% endif %}
{% if user.is_active or user.deactivation_reason == "pending" %} {% if user.is_active or user.deactivation_reason == "pending" %}
<form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id %}" class="mr-1"> <form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id %}" class="mr-1">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button> <button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button>
</form> </form>
{% else %} {% else %}
<form name="unsuspend" method="post" action="{% url 'settings-report-unsuspend' user.id %}" class="mr-1"> <form name="unsuspend" method="post" action="{% url 'settings-report-unsuspend' user.id %}" class="mr-1">
{% csrf_token %} {% csrf_token %}
<button class="button">{% trans "Un-suspend user" %}</button> <button class="button">{% trans "Un-suspend user" %}</button>
</form> </form>
{% endif %}
{% if user.local %}
<div>
{% trans "Permanently delete user" as button_text %}
{% include "snippets/toggle/open_button.html" with controls_text="delete_user" text=button_text class="is-danger is-light" %}
</div>
{% endif %}
</div>
{% if user.local %}
<div>
{% include "settings/users/delete_user_form.html" with controls_text="delete_user" class="mt-2 mb-2" %}
</div>
{% endif %} {% endif %}
{% if user.local %} {% if user.local %}
<div> <div>
{% trans "Permanently delete user" as button_text %} <form name="permission" method="post" action="{% url 'settings-user' user.id %}">
{% include "snippets/toggle/open_button.html" with controls_text="delete_user" text=button_text class="is-danger is-light" %} {% csrf_token %}
<label class="label" for="id_user_group">{% trans "Access level:" %}</label>
{% if group_form.non_field_errors %}
{{ group_form.non_field_errors }}
{% endif %}
{% with group=user.groups.first %}
<div class="select">
<select name="groups" id="id_user_group" aria-describedby="desc_user_group">
{% for value, name in group_form.fields.groups.choices %}
<option value="{{ value }}" {% if name == group.name %}selected{% endif %}>
{{ name|title }}
</option>
{% endfor %}
<option value="" {% if not group %}selected{% endif %}>
User
</option>
</select>
</div>
{% include 'snippets/form_errors.html' with errors_list=group_form.groups.errors id="desc_user_group" %}
{% endwith %}
<button class="button">
{% trans "Save" %}
</button>
</form>
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% if user.local %}
<div>
{% include "settings/users/delete_user_form.html" with controls_text="delete_user" class="mt-2 mb-2" %}
</div>
{% endif %}
{% if user.local %}
<div>
<form name="permission" method="post" action="{% url 'settings-user' user.id %}">
{% csrf_token %}
<label class="label" for="id_user_group">{% trans "Access level:" %}</label>
{% if group_form.non_field_errors %}
{{ group_form.non_field_errors }}
{% endif %}
{% with group=user.groups.first %}
<div class="select">
<select name="groups" id="id_user_group" aria-describedby="desc_user_group">
{% for value, name in group_form.fields.groups.choices %}
<option value="{{ value }}" {% if name == group.name %}selected{% endif %}>
{{ name|title }}
</option>
{% endfor %}
<option value="" {% if not group %}selected{% endif %}>
User
</option>
</select>
</div>
{% include 'snippets/form_errors.html' with errors_list=group_form.groups.errors id="desc_user_group" %}
{% endwith %}
<button class="button">
{% trans "Save" %}
</button>
</form>
</div>
{% endif %}
{% endif %} {% endif %}
</div> </div>

View file

@ -1,5 +1,5 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load bookwyrm_tags %} {% load shelf_tags %}
{% load utilities %} {% load utilities %}
{% load humanize %} {% load humanize %}
{% load i18n %} {% load i18n %}
@ -19,6 +19,17 @@
</h1> </h1>
</header> </header>
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
<ul>
<li><a href="{% url 'user-feed' user|username %}">{% trans "User profile" %}</a></li>
<li class="is-active">
<a href="#" aria-current="page">
{% include "snippets/translated_shelf_name.html" with shelf=shelf %}
</a>
</li>
</ul>
</nav>
<nav class="block columns is-mobile scroll-x"> <nav class="block columns is-mobile scroll-x">
<div class="column pr-0"> <div class="column pr-0">
<div class="tabs"> <div class="tabs">
@ -34,15 +45,7 @@
href="{{ shelf_tab.local_path }}" href="{{ shelf_tab.local_path }}"
{% if shelf_tab.identifier == shelf.identifier %} aria-current="page"{% endif %} {% if shelf_tab.identifier == shelf.identifier %} aria-current="page"{% endif %}
> >
{% if shelf_tab.identifier == 'to-read' %} {% include 'user/books_header.html' with shelf=shelf_tab %}
{% trans "To Read" %}
{% elif shelf_tab.identifier == 'reading' %}
{% trans "Currently Reading" %}
{% elif shelf_tab.identifier == 'read' %}
{% trans "Read" %}
{% else %}
{{ shelf_tab.name }}
{% endif %}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}
@ -89,7 +92,7 @@
</span> </span>
{% with count=books.paginator.count %} {% with count=books.paginator.count %}
{% if count %} {% if count %}
<p class="help"> <span class="help">
{% blocktrans trimmed count counter=count with formatted_count=count|intcomma %} {% blocktrans trimmed count counter=count with formatted_count=count|intcomma %}
{{ formatted_count }} book {{ formatted_count }} book
{% plural %} {% plural %}
@ -101,7 +104,7 @@
(showing {{ start }}-{{ end }}) (showing {{ start }}-{{ end }})
{% endblocktrans %} {% endblocktrans %}
{% endif %} {% endif %}
</p> </span>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
</h2> </h2>

View file

@ -1,5 +1,6 @@
{% load i18n %} {% load i18n %}
{% load bookwyrm_group_tags %} {% load group_tags %}
{% if request.user == user or not request.user == group.user or not request.user.is_authenticated %} {% if request.user == user or not request.user == group.user or not request.user.is_authenticated %}
{% elif user in request.user.blocks.all %} {% elif user in request.user.blocks.all %}
{% include 'snippets/block_button.html' with blocks=True %} {% include 'snippets/block_button.html' with blocks=True %}

View file

@ -1,5 +1,5 @@
{% extends "snippets/create_status/layout.html" %} {% extends "snippets/create_status/layout.html" %}
{% load bookwyrm_tags %} {% load shelf_tags %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}
{% load status_display %} {% load status_display %}

View file

@ -1,4 +1,3 @@
{% load bookwyrm_tags %}
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}
{% load status_display %} {% load status_display %}

View file

@ -1,5 +1,4 @@
{% extends "snippets/create_status/layout.html" %} {% extends "snippets/create_status/layout.html" %}
{% load bookwyrm_tags %}
{% load utilities %} {% load utilities %}
{% load status_display %} {% load status_display %}
{% load i18n %} {% load i18n %}

View file

@ -1,5 +1,4 @@
{% extends "snippets/create_status/layout.html" %} {% extends "snippets/create_status/layout.html" %}
{% load bookwyrm_tags %}
{% load utilities %} {% load utilities %}
{% load status_display %} {% load status_display %}
{% load i18n %} {% load i18n %}

View file

@ -3,16 +3,15 @@
<summary class="is-flex is-align-items-center is-flex-wrap-wrap is-gap-2"> <summary class="is-flex is-align-items-center is-flex-wrap-wrap is-gap-2">
<span class="mb-0 title {% if size == 'small' %}is-6{% else %}is-5{% endif %} is-flex-shrink-0"> <span class="mb-0 title {% if size == 'small' %}is-6{% else %}is-5{% endif %} is-flex-shrink-0">
{% trans "Filters" %} {% trans "Filters" %}
</span> </span>
{% if filters_applied %} {% if filters_applied %}
<span class="tag is-success is-light ml-2 mb-0 is-{{ size|default:'normal' }}"> <span class="tag is-success is-light ml-2 mb-0 is-{{ size|default:'normal' }}">
{{ _("Filters are applied") }} {% trans "Filters are applied" %}
</span> </span>
{% endif %} {% endif %}
{% if request.GET %} {% if method != "post" and request.GET %}
<span class="mb-0 tags has-addons"> <span class="mb-0 tags has-addons">
<span class="mb-0 tag is-success is-light is-{{ size|default:'normal' }}"> <span class="mb-0 tag is-success is-light is-{{ size|default:'normal' }}">
{% trans "Filters are applied" %} {% trans "Filters are applied" %}

View file

@ -1,13 +1,18 @@
{% load i18n %} {% load i18n %}
{% load interaction %}
{% if request.user == user or not request.user.is_authenticated %} {% if request.user == user or not request.user.is_authenticated %}
{% elif user in request.user.blocks.all %} {# nothing to see here -- either it's yourself or your logged out #}
{% else %}
{% get_relationship user as relationship %}
{% if relationship.is_blocked %}
{% include 'snippets/block_button.html' with blocks=True %} {% include 'snippets/block_button.html' with blocks=True %}
{% else %} {% else %}
<div class="field{% if not minimal %} has-addons{% else %} mb-0{% endif %}"> <div class="field{% if not minimal %} has-addons{% else %} mb-0{% endif %}">
<div class="control"> <div class="control">
<form action="{% url 'follow' %}" method="POST" class="interaction follow_{{ user.id }} {% if request.user in user.followers.all or request.user in user.follower_requests.all %}is-hidden{%endif %}" data-id="follow_{{ user.id }}"> <form action="{% url 'follow' %}" method="POST" class="interaction follow_{{ user.id }} {% if relationship.is_following or relationship.is_follow_pending %}is-hidden{%endif %}" data-id="follow_{{ user.id }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}"> <input type="hidden" name="user" value="{{ user.username }}">
<button class="button is-small{% if not minimal %} is-link{% endif %}" type="submit"> <button class="button is-small{% if not minimal %} is-link{% endif %}" type="submit">
@ -18,10 +23,10 @@
{% endif %} {% endif %}
</button> </button>
</form> </form>
<form action="{% url 'unfollow' %}" method="POST" class="interaction follow_{{ user.id }} {% if not request.user in user.followers.all and not request.user in user.follower_requests.all %}is-hidden{%endif %}" data-id="follow_{{ user.id }}"> <form action="{% url 'unfollow' %}" method="POST" class="interaction follow_{{ user.id }} {% if not relationship.is_following and not relationship.is_follow_pending %}is-hidden{%endif %}" data-id="follow_{{ user.id }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}"> <input type="hidden" name="user" value="{{ user.username }}">
{% if user.manually_approves_followers and request.user not in user.followers.all %} {% if user.manually_approves_followers and not relationship.is_following %}
<button class="button is-small is-danger is-light" type="submit"> <button class="button is-small is-danger is-light" type="submit">
{% trans "Undo follow request" %} {% trans "Undo follow request" %}
</button> </button>
@ -42,4 +47,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endif %}
{% endif %} {% endif %}

View file

@ -1,10 +1,16 @@
{% load i18n %} {% load i18n %}
{% if rating %} {% if rating %}
{% blocktrans with book_title=book.title|safe display_rating=rating|floatformat:"-1" review_title=name|safe count counter=rating %}Review of "{{ book_title }}" ({{ display_rating }} star): {{ review_title }}{% plural %}Review of "{{ book_title }}" ({{ display_rating }} stars): {{ review_title }}{% endblocktrans %} {% blocktrans trimmed with book_title=book.title|safe book_path=book.local_path display_rating=rating|floatformat:"-1" review_title=name|safe count counter=rating %}
Review of "{{ book_title }}" ({{ display_rating }} star): {{ review_title }}
{% plural %}
Review of "{{ book_title }}" ({{ display_rating }} stars): {{ review_title }}
{% endblocktrans %}
{% else %} {% else %}
{% blocktrans with book_title=book.title|safe review_title=name|safe %}Review of "{{ book_title }}": {{ review_title }}{% endblocktrans %} {% blocktrans trimmed with book_title=book.title|safe book_path=book.local_path review_title=name|safe %}
Review of "{{ book_title }}": {{ review_title }}
{% endblocktrans %}
{% endif %} {% endif %}

View file

@ -1,5 +1,6 @@
{% load i18n %} {% load i18n %}
{% load bookwyrm_group_tags %} {% load group_tags %}
{% if group|is_invited:request.user %} {% if group|is_invited:request.user %}
<div class="field is-grouped"> <div class="field is-grouped">
<form action="/accept-group-invitation/" method="POST"> <form action="/accept-group-invitation/" method="POST">

View file

@ -1,5 +1,6 @@
{% load i18n %} {% load i18n %}
{% load bookwyrm_tags %} {% load rating_tags %}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<span class="is-sr-only">{% trans "Leave a rating" %}</span> <span class="is-sr-only">{% trans "Leave a rating" %}</span>
<div class="block"> <div class="block">

View file

@ -1,5 +1,6 @@
{% load i18n %} {% load i18n %}
{% load bookwyrm_group_tags %} {% load group_tags %}
{% if request.user == user or not request.user == group.user or not request.user.is_authenticated %} {% if request.user == user or not request.user == group.user or not request.user.is_authenticated %}
{% else %} {% else %}
{% if user in request.user.blocks.all %} {% if user in request.user.blocks.all %}

View file

@ -12,6 +12,6 @@
> >
{% trans "Report" %} {% trans "Report" %}
</button> </button>
{% include 'snippets/report_modal.html' with user=user reporter=request.user id=modal_id %} {% include 'snippets/report_modal.html' with user=user id=modal_id status=status.id %}
{% endwith %} {% endwith %}

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