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_PORT=6379
REDIS_ACTIVITY_PASSWORD=redispassword345
# Optional, use a different redis database (defaults to 0)
# REDIS_ACTIVITY_DB_INDEX=0
# Redis as celery broker
REDIS_BROKER_PORT=6379
REDIS_BROKER_PASSWORD=redispassword123
# Optional, use a different redis database (defaults to 0)
# REDIS_BROKER_DB_INDEX=0
# Monitoring for celery
FLOWER_PORT=8888

View file

@ -20,22 +20,6 @@ class ActivityEncoder(JSONEncoder):
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
# pylint: disable=invalid-name
class Signature:
@ -198,8 +182,9 @@ class ActivityObject:
)
return instance
def serialize(self):
def serialize(self, **kwargs):
"""convert to dictionary with context attr"""
omit = kwargs.get("omit", ())
data = self.__dict__.copy()
# recursively serialize
for (k, v) in data.items():
@ -208,8 +193,9 @@ class ActivityObject:
data[k] = v.serialize()
except TypeError:
pass
data = {k: v for (k, v) in data.items() if v is not None}
data["@context"] = "https://www.w3.org/ns/activitystreams"
data = {k: v for (k, v) in data.items() if v is not None and k not in omit}
if "@context" not in omit:
data["@context"] = "https://www.w3.org/ns/activitystreams"
return data
@ -222,35 +208,32 @@ def set_related_field(
model = apps.get_model(f"bookwyrm.{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):
existing = model.find_existing_by_remote_id(data)
if existing:
data = existing.to_activity()
else:
data = get_data(data)
activity = model.activity_serializer(**data)
if isinstance(data, str):
existing = model.find_existing_by_remote_id(data)
if existing:
data = existing.to_activity()
else:
data = get_data(data)
activity = model.activity_serializer(**data)
# this must exist because it's the object that triggered this function
instance = origin_model.find_existing_by_remote_id(related_remote_id)
if not instance:
raise ValueError(f"Invalid related remote id: {related_remote_id}")
# this must exist because it's the object that triggered this function
instance = origin_model.find_existing_by_remote_id(related_remote_id)
if not instance:
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
# the model instance is created
# edition.parentWork = instance, for example
model_field = getattr(model, related_field_name)
if hasattr(model_field, "activitypub_field"):
setattr(
activity, getattr(model_field, "activitypub_field"), instance.remote_id
)
item = activity.to_model()
# set the origin's remote id on the activity so it will be there when
# the model instance is created
# edition.parentWork = instance, for example
model_field = getattr(model, related_field_name)
if hasattr(model_field, "activitypub_field"):
setattr(activity, getattr(model_field, "activitypub_field"), instance.remote_id)
item = activity.to_model()
# if the related field isn't serialized (attachments on Status), then
# we have to set it post-creation
if not hasattr(model_field, "activitypub_field"):
setattr(item, related_field_name, instance)
item.save()
# if the related field isn't serialized (attachments on Status), then
# we have to set it post-creation
if not hasattr(model_field, "activitypub_field"):
setattr(item, related_field_name, instance)
item.save()
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
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
bnfId: str = None
lastEditedBy: str = None
links: List[str] = field(default_factory=lambda: [])
fileLinks: List[str] = field(default_factory=lambda: [])
# pylint: disable=invalid-name

View file

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

View file

@ -216,6 +216,18 @@ class CoverForm(CustomForm):
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 Meta:
model = models.Edition
@ -230,6 +242,8 @@ class EditionForm(CustomForm):
"shelves",
"connector",
"search_vector",
"links",
"file_links",
]
widgets = {
"title": forms.TextInput(attrs={"aria-describedby": "desc_title"}),
@ -439,7 +453,7 @@ class GroupForm(CustomForm):
class ReportForm(CustomForm):
class Meta:
model = models.Report
fields = ["user", "reporter", "statuses", "note"]
fields = ["user", "reporter", "statuses", "links", "note"]
class EmailBlocklistForm(CustomForm):
@ -478,3 +492,19 @@ class SortListForm(forms.Form):
("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
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.contenttypes.models import ContentType
from bookwyrm.models import Connector, FederatedServer, SiteSettings, User
from bookwyrm import models
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:
permission_obj = Permission.objects.create(
codename=permission["codename"],
@ -72,7 +72,7 @@ def init_permissions():
def init_connectors():
"""access book data sources"""
Connector.objects.create(
models.Connector.objects.create(
identifier="bookwyrm.social",
name="BookWyrm dot Social",
connector_file="bookwyrm_connector",
@ -84,7 +84,7 @@ def init_connectors():
priority=2,
)
Connector.objects.create(
models.Connector.objects.create(
identifier="inventaire.io",
name="Inventaire",
connector_file="inventaire",
@ -96,7 +96,7 @@ def init_connectors():
priority=3,
)
Connector.objects.create(
models.Connector.objects.create(
identifier="openlibrary.org",
name="OpenLibrary",
connector_file="openlibrary",
@ -113,7 +113,7 @@ def init_federated_servers():
"""big no to nazis"""
built_in_blocks = ["gab.ai", "gab.com"]
for server in built_in_blocks:
FederatedServer.objects.create(
models.FederatedServer.objects.create(
server_name=server,
status="blocked",
)
@ -121,18 +121,61 @@ def init_federated_servers():
def init_settings():
"""info about the instance"""
SiteSettings.objects.create(
models.SiteSettings.objects.create(
support_link="https://www.patreon.com/bookwyrm",
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):
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):
init_groups()
init_permissions()
init_connectors()
init_federated_servers()
init_settings()
limit = options.get("limit")
tables = [
"group",
"permission",
"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"
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
def handle(self, *args, **options):
"""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 .author import Author
from .link import Link, FileLink, LinkDomain
from .connector import Connector
from .shelf import Shelf, ShelfBook

View file

@ -241,8 +241,11 @@ class Work(OrderedCollectionPageMixin, Book):
)
activity_serializer = activitypub.Work
serialize_reverse_fields = [("editions", "editions", "-edition_rank")]
deserialize_reverse_fields = [("editions", "editions")]
serialize_reverse_fields = [
("editions", "editions", "-edition_rank"),
("file_links", "fileLinks", "-created_date"),
]
deserialize_reverse_fields = [("editions", "editions"), ("file_links", "fileLinks")]
# https://schema.org/BookFormatType
@ -296,6 +299,8 @@ class Edition(Book):
activity_serializer = activitypub.Edition
name_field = "title"
serialize_reverse_fields = [("file_links", "fileLinks", "-created_date")]
deserialize_reverse_fields = [("file_links", "fileLinks")]
def get_rank(self):
"""calculate how complete the data is on this edition"""
@ -337,6 +342,11 @@ class Edition(Book):
# set 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)
@classmethod

View file

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

View file

@ -40,9 +40,7 @@ class ImportJob(models.Model):
mappings = models.JSONField()
complete = models.BooleanField(default=False)
source = models.CharField(max_length=100)
privacy = models.CharField(
max_length=255, default="public", choices=PrivacyLevels.choices
)
privacy = models.CharField(max_length=255, default="public", choices=PrivacyLevels)
retry = models.BooleanField(default=False)
@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 """
from django.core import validators
from django.core.cache import cache
from django.db import models
from django.db.models import F, Q
@ -30,6 +31,7 @@ class ReadThrough(BookWyrmModel):
def save(self, *args, **kwargs):
"""update user active time"""
cache.delete(f"latest_read_through-{self.user.id}-{self.book.id}")
self.user.update_active_date()
# an active readthrough must have an unset finish date
if self.finish_date:

View file

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

View file

@ -1,6 +1,5 @@
""" flagged for moderation """
from django.db import models
from django.db.models import F, Q
from .base_model import BookWyrmModel
@ -13,14 +12,12 @@ class Report(BookWyrmModel):
note = models.TextField(null=True, blank=True)
user = models.ForeignKey("User", on_delete=models.PROTECT)
statuses = models.ManyToManyField("Status", blank=True)
links = models.ManyToManyField("Link", blank=True)
resolved = models.BooleanField(default=False)
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",)

View file

@ -1,5 +1,6 @@
""" puttin' books on shelves """
import re
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.db import models
from django.utils import timezone
@ -94,8 +95,15 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel):
def save(self, *args, **kwargs):
if not self.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)
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:
"""an opinionated constraint!
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 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):
"""gives someone access to create an account on the instance"""

View file

@ -3,6 +3,7 @@ from dataclasses import MISSING
import re
from django.apps import apps
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
@ -373,6 +374,12 @@ class Review(BookStatus):
activity_serializer = activitypub.Review
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):
"""a subtype of review that only contains a rating"""

View file

@ -129,7 +129,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
related_name="favorite_statuses",
)
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")
created_date = models.DateTimeField(auto_now_add=True)
@ -346,6 +346,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
def delete(self, *args, **kwargs):
"""deactivate rather than delete a user"""
# pylint: disable=attribute-defined-outside-init
self.is_active = False
# skip the logic in this class's save()
super().save(*args, **kwargs)
@ -406,14 +407,6 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
self.private_key, self.public_key = create_key_pair()
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():
"""sets default year for annual goal to this year"""
@ -427,7 +420,7 @@ class AnnualGoal(BookWyrmModel):
goal = models.IntegerField(validators=[MinValueValidator(1)])
year = models.IntegerField(default=get_current_year)
privacy = models.CharField(
max_length=255, default="public", choices=fields.PrivacyLevels.choices
max_length=255, default="public", choices=fields.PrivacyLevels
)
class Meta:

View file

@ -8,7 +8,7 @@ r = redis.Redis(
host=settings.REDIS_ACTIVITY_HOST,
port=settings.REDIS_ACTIVITY_PORT,
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.read_env()
DOMAIN = env("DOMAIN")
VERSION = "0.1.1"
VERSION = "0.2.0"
PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
JS_CACHE = "2d3181e1"
JS_CACHE = "76c5ff1f"
# email
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_SSL = env.bool("EMAIL_USE_SSL", False)
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}"
# 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"
@ -113,6 +165,7 @@ WSGI_APPLICATION = "bookwyrm.wsgi.application"
REDIS_ACTIVITY_HOST = env("REDIS_ACTIVITY_HOST", "localhost")
REDIS_ACTIVITY_PORT = env("REDIS_ACTIVITY_PORT", 6379)
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))
@ -139,7 +192,7 @@ else:
CACHES = {
"default": {
"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": {
"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
******************************************************************************/
@ -751,6 +756,13 @@ ol.ordered-list li::before {
padding: 0 0.75em;
}
/* Notifications page
******************************************************************************/
.notification a.icon {
text-decoration: none !important;
}
/* 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)));
document
.querySelectorAll("button[data-modal-open]")
.querySelectorAll("[data-modal-open]")
.forEach((node) => node.addEventListener("click", this.handleModalButton.bind(this)));
document

View file

@ -2,7 +2,7 @@
{% load humanize %}
{% load i18n %}
{% load utilities %}
{% load bookwyrm_tags %}
{% load landing_page_tags %}
{% load cache %}
{% block title %}
@ -12,6 +12,7 @@
{% block about_content %}
{# seven day cache #}
{% cache 604800 about_page %}
{% get_book_superlatives as superlatives %}
<section class="content pb-4">
<h2>
@ -26,7 +27,7 @@
</p>
<div class="columns">
{% if top_rated %}
{% if superlatives.top_rated %}
{% with book=superlatives.top_rated.default_edition rating=top_rated.rating %}
<div class="column is-one-third is-flex">
<div class="media notification">
@ -45,7 +46,7 @@
{% endwith %}
{% endif %}
{% if wanted %}
{% if superlatives.wanted %}
{% with book=superlatives.wanted.default_edition %}
<div class="column is-one-third is-flex">
<div class="media notification">
@ -64,7 +65,7 @@
{% endwith %}
{% endif %}
{% if controversial %}
{% if superlatives.controversial %}
{% with book=superlatives.controversial.default_edition %}
<div class="column is-one-third is-flex">
<div class="media notification">
@ -95,7 +96,7 @@
<h2 class="title is-3">{% trans "Meet your admins" %}</h2>
<p>
{% 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.
{% endblocktrans %}
</p>

View file

@ -23,18 +23,13 @@
</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 }}">
{% 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.wikipedia_link author.openlibrary_key author.inventaire_id author.isni as links %}
{% if details or links %}
<div class="column is-two-fifths">
<div class="column is-3">
{% if details %}
<section class="block content">
<h2 class="title is-4">{% trans "Author details" %}</h2>
@ -71,7 +66,7 @@
<div class="box">
{% if author.wikipedia_link %}
<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" %}
</a>
</div>
@ -79,7 +74,7 @@
{% if author.isni %}
<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" %}
</a>
</div>
@ -88,7 +83,7 @@
{% trans "Load data" as button_text %}
{% if author.openlibrary_key %}
<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" %}
</a>
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
@ -103,7 +98,7 @@
{% if author.inventaire_id %}
<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" %}
</a>
@ -119,7 +114,7 @@
{% if author.librarything_key %}
<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" %}
</a>
</div>
@ -127,7 +122,7 @@
{% if author.goodreads_key %}
<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" %}
</a>
</div>
@ -137,26 +132,30 @@
{% endif %}
</div>
{% 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>
<div class="columns is-multiline is-mobile">
{% for book in books %}
<div class="column is-one-fifth-tablet is-half-mobile is-flex is-flex-direction-column">
<div class="is-flex-grow-1">
{% include 'landing/small-book.html' with book=book %}
<h2 class="title is-4">{% blocktrans with name=author.name %}Books by {{ name }}{% endblocktrans %}</h2>
<div class="columns is-multiline is-mobile">
{% 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="is-flex-grow-1">
{% include 'landing/small-book.html' with book=book %}
</div>
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
</div>
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
{% endwith %}
{% endfor %}
</div>
<div>
{% include 'snippets/pagination.html' with page=books %}
</div>
{% endfor %}
</div>
</div>
<div>
{% include 'snippets/pagination.html' with page=books %}
</div>
{% endblock %}

View file

@ -1,6 +1,6 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load book_display_tags %}
{% load humanize %}
{% load utilities %}
{% load static %}
@ -122,7 +122,7 @@
{% trans "Load data" as button_text %}
{% if book.openlibrary_key %}
<p>
<a href="{{ book.openlibrary_link }}" target="_blank" rel="noopener">
<a href="{{ book.openlibrary_link }}" target="_blank" rel="noopener noreferrer">
{% trans "View on OpenLibrary" %}
</a>
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
@ -136,7 +136,7 @@
{% endif %}
{% if book.inventaire_id %}
<p>
<a href="{{ book.inventaire_link }}" target="_blank" rel="noopener">
<a href="{{ book.inventaire_link }}" target="_blank" rel="noopener noreferrer">
{% trans "View on Inventaire" %}
</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" %}
<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 %}
<p class="fields is-grouped">
<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>
</div>
<div class="column is-narrow">
{% trans "Add read dates" as button_text %}
{% 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_" %}
<button class="button is-small" data-modal-open="add-readthrough">
<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>
</header>
<section class="is-hidden box" 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>
{% include "readthrough/readthrough_modal.html" with id="add-readthrough" %}
{% if not readthroughs.exists %}
<p>{% trans "You don't have any reading activity for this book." %}</p>
{% endif %}
{% for readthrough in readthroughs %}
{% include 'book/readthrough.html' with readthrough=readthrough %}
{% include 'readthrough/readthrough_list.html' with readthrough=readthrough %}
{% endfor %}
</section>
<hr aria-hidden="true">
@ -327,7 +319,7 @@
</div>
</div>
</div>
<div class="column is-one-fifth">
<div class="column is-one-fifth is-clipped">
{% if book.subjects %}
<section class="content block">
<h2 class="title is-5">{% trans "Subjects" %}</h2>
@ -352,11 +344,11 @@
{% endif %}
{% 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>
<ul>
{% 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 %}
</ul>
@ -366,7 +358,7 @@
<input type="hidden" name="book" value="{{ book.id }}">
<label class="label" for="id_list">{% trans "Add to list" %}</label>
<div class="field has-addons">
<div class="select control">
<div class="select control is-clipped">
<select name="list" id="id_list">
{% for list in user.list_set.all %}
<option value="{{ list.id }}">{{ list.name }}</option>
@ -381,6 +373,10 @@
{% endif %}
</section>
{% endif %}
<section class="content block">
{% include "book/file_links/links.html" %}
</section>
</div>
</div>
@ -390,4 +386,5 @@
{% block scripts %}
<script src="{% static "js/vendor/tabs.js" %}?v={{ js_cache }}"></script>
<script src="{% static "js/autocomplete.js" %}?v={{ js_cache }}"></script>
{% 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 utilities %}
{% load status_display %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,6 @@
{% 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 %}

View file

@ -1,13 +1,17 @@
{% extends 'landing/layout.html' %}
{% load i18n %}
{% load cache %}
{% load landing_page_tags %}
{% block panel %}
<div class="block is-hidden-tablet">
<h2 class="title has-text-centered">{% trans "Recent Books" %}</h2>
</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">
<div class="tile is-vertical is-6">
<div class="tile is-parent">

View file

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

View file

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

View file

@ -1,7 +1,8 @@
{% extends 'embed-layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load bookwyrm_group_tags %}
{% load book_display_tags %}
{% load rating_tags %}
{% load group_tags %}
{% load markdown %}
{% 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' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load bookwyrm_group_tags %}
{% load rating_tags %}
{% load book_display_tags %}
{% load group_tags %}
{% load markdown %}
{% block breadcrumbs %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,26 +34,26 @@
<div class="column">
{{ 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 class="field">
<label class="label" for="id_name">{% trans "Display name:" %}</label>
{{ 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 class="field">
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
{{ 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 class="field">
<label class="label" for="id_email">{% trans "Email address:" %}</label>
{{ 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>
</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 %}
<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" %}

View file

@ -3,7 +3,7 @@
{% load tz %}
{% load utilities %}
<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="column">
{% trans "Progress Updates:" %}
@ -58,7 +58,11 @@
<div class="field has-addons">
<div class="control">
{% 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 class="control">
{% trans "Delete these read dates" as button_text %}
@ -74,16 +78,7 @@
</div>
</div>
<div class="box is-hidden" id="edit_readthrough_{{ readthrough.id }}" tabindex="0">
<h3 class="title is-5">{% trans "Edit read dates" %}</h3>
<form name="edit-readthrough" action="/edit-readthrough" method="post">
{% include 'snippets/readthrough_form.html' with readthrough=readthrough %}
<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 %}
{% join "edit_readthrough" readthrough.id as edit_modal_id %}
{% include "readthrough/readthrough_modal.html" with readthrough=readthrough id=edit_modal_id %}
{% join "delete_readthrough" readthrough.id as delete_modal_id %}
{% include 'readthrough/delete_readthrough_modal.html' with id=delete_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>
<a
href="{{ result.view_link|default:result.key }}"
rel="noopener"
rel="noopener noreferrer"
target="_blank"
>{{ result.title }}</a>
</strong>
@ -75,9 +75,11 @@
<form class="mt-1" action="/resolve-book" method="post">
{% csrf_token %}
<input type="hidden" name="remote_id" value="{{ result.key }}">
<button type="submit" class="button is-small is-link">
{% trans "Import book" %}
</button>
<div class="control">
<button type="submit" class="button is-small is-link">
{% trans "Import book" %}
</button>
</div>
</form>
</div>
</div>

View file

@ -48,6 +48,17 @@
</a>
</div>
{% 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 %}
<div class="column">
<a href="{% url 'settings-invite-requests' %}" class="notification is-block is-success is-light">

View file

@ -47,7 +47,7 @@
<div class="field">
<label class="label" for="id_file">JSON data:</label>
<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>
[
{

View file

@ -62,6 +62,10 @@
{% 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>
</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>
{% endif %}
{% 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 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 %}
{% 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>
{% endblock %}
@ -15,6 +17,36 @@
{% include 'settings/reports/report_preview.html' with report=report %}
</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_moderation_actions.html' with user=report.user %}
@ -41,23 +73,4 @@
<button class="button">{% trans "Comment" %}</button>
</form>
</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 %}

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' %}
{% load i18n %}
{% load humanize %}
{% load utilities %}
{% block card-header %}
<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>
{% endblock %}
@ -17,7 +21,7 @@
{% block card-footer %}
<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 class="card-footer-item">
{{ report.created_date | naturaltime }}

View file

@ -5,71 +5,73 @@
{% trans "Permanently deleted" %}
</div>
{% else %}
<h3>{% trans "Actions" %}</h3>
<h3>{% trans "User Actions" %}</h3>
<div class="is-flex">
{% if user.is_active %}
<p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a>
</p>
{% endif %}
<div class="box">
<div class="is-flex">
{% if user.is_active %}
<p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a>
</p>
{% endif %}
{% if user.is_active or user.deactivation_reason == "pending" %}
<form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id %}" class="mr-1">
{% csrf_token %}
<button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button>
</form>
{% else %}
<form name="unsuspend" method="post" action="{% url 'settings-report-unsuspend' user.id %}" class="mr-1">
{% csrf_token %}
<button class="button">{% trans "Un-suspend user" %}</button>
</form>
{% if user.is_active or user.deactivation_reason == "pending" %}
<form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id %}" class="mr-1">
{% csrf_token %}
<button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button>
</form>
{% else %}
<form name="unsuspend" method="post" action="{% url 'settings-report-unsuspend' user.id %}" class="mr-1">
{% csrf_token %}
<button class="button">{% trans "Un-suspend user" %}</button>
</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 %}
{% 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" %}
<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 %}
</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 %}
</div>

View file

@ -1,5 +1,5 @@
{% extends 'layout.html' %}
{% load bookwyrm_tags %}
{% load shelf_tags %}
{% load utilities %}
{% load humanize %}
{% load i18n %}
@ -19,6 +19,17 @@
</h1>
</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">
<div class="column pr-0">
<div class="tabs">
@ -34,15 +45,7 @@
href="{{ shelf_tab.local_path }}"
{% if shelf_tab.identifier == shelf.identifier %} aria-current="page"{% endif %}
>
{% if shelf_tab.identifier == 'to-read' %}
{% trans "To Read" %}
{% elif shelf_tab.identifier == 'reading' %}
{% trans "Currently Reading" %}
{% elif shelf_tab.identifier == 'read' %}
{% trans "Read" %}
{% else %}
{{ shelf_tab.name }}
{% endif %}
{% include 'user/books_header.html' with shelf=shelf_tab %}
</a>
</li>
{% endfor %}
@ -89,7 +92,7 @@
</span>
{% with count=books.paginator.count %}
{% if count %}
<p class="help">
<span class="help">
{% blocktrans trimmed count counter=count with formatted_count=count|intcomma %}
{{ formatted_count }} book
{% plural %}
@ -101,7 +104,7 @@
(showing {{ start }}-{{ end }})
{% endblocktrans %}
{% endif %}
</p>
</span>
{% endif %}
{% endwith %}
</h2>

View file

@ -1,5 +1,6 @@
{% 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 %}
{% elif user in request.user.blocks.all %}
{% include 'snippets/block_button.html' with blocks=True %}

View file

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

View file

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

View file

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

View file

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

View file

@ -3,16 +3,15 @@
<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">
{% trans "Filters" %}
</span>
{% if filters_applied %}
<span class="tag is-success is-light ml-2 mb-0 is-{{ size|default:'normal' }}">
{{ _("Filters are applied") }}
{% trans "Filters are applied" %}
</span>
{% endif %}
{% if request.GET %}
{% if method != "post" and request.GET %}
<span class="mb-0 tags has-addons">
<span class="mb-0 tag is-success is-light is-{{ size|default:'normal' }}">
{% trans "Filters are applied" %}

View file

@ -1,13 +1,18 @@
{% load i18n %}
{% load interaction %}
{% 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 %}
{% else %}
<div class="field{% if not minimal %} has-addons{% else %} mb-0{% endif %}">
<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 %}
<input type="hidden" name="user" value="{{ user.username }}">
<button class="button is-small{% if not minimal %} is-link{% endif %}" type="submit">
@ -18,10 +23,10 @@
{% endif %}
</button>
</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 %}
<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">
{% trans "Undo follow request" %}
</button>
@ -42,4 +47,7 @@
</div>
{% endif %}
</div>
{% endif %}
{% endif %}

View file

@ -1,10 +1,16 @@
{% load i18n %}
{% 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 %}
{% 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 %}

View file

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

View file

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

View file

@ -1,5 +1,6 @@
{% 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 %}
{% else %}
{% if user in request.user.blocks.all %}

View file

@ -12,6 +12,6 @@
>
{% trans "Report" %}
</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 %}

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