Merge branch 'production' into nix
This commit is contained in:
commit
2c5e57c602
175 changed files with 13847 additions and 4677 deletions
|
@ -8,6 +8,8 @@ USE_HTTPS=true
|
||||||
DOMAIN=your.domain.here
|
DOMAIN=your.domain.here
|
||||||
EMAIL=your@email.here
|
EMAIL=your@email.here
|
||||||
|
|
||||||
|
# Instance defualt language (see options at bookwyrm/settings.py "LANGUAGES"
|
||||||
|
LANGUAGE_CODE="en-us"
|
||||||
# Used for deciding which editions to prefer
|
# Used for deciding which editions to prefer
|
||||||
DEFAULT_LANGUAGE="English"
|
DEFAULT_LANGUAGE="English"
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ class Create(Verb):
|
||||||
class Delete(Verb):
|
class Delete(Verb):
|
||||||
"""Create activity"""
|
"""Create activity"""
|
||||||
|
|
||||||
to: List[str]
|
to: List[str] = field(default_factory=lambda: [])
|
||||||
cc: List[str] = field(default_factory=lambda: [])
|
cc: List[str] = field(default_factory=lambda: [])
|
||||||
type: str = "Delete"
|
type: str = "Delete"
|
||||||
|
|
||||||
|
@ -137,8 +137,8 @@ class Accept(Verb):
|
||||||
type: str = "Accept"
|
type: str = "Accept"
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
"""find and remove the activity object"""
|
"""accept a request"""
|
||||||
obj = self.object.to_model(save=False, allow_create=False)
|
obj = self.object.to_model(save=False, allow_create=True)
|
||||||
obj.accept()
|
obj.accept()
|
||||||
|
|
||||||
|
|
||||||
|
@ -150,7 +150,7 @@ class Reject(Verb):
|
||||||
type: str = "Reject"
|
type: str = "Reject"
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
"""find and remove the activity object"""
|
"""reject a follow request"""
|
||||||
obj = self.object.to_model(save=False, allow_create=False)
|
obj = self.object.to_model(save=False, allow_create=False)
|
||||||
obj.reject()
|
obj.reject()
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,34 @@
|
||||||
|
"""Do further startup configuration and initialization"""
|
||||||
|
import os
|
||||||
|
import urllib
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
from bookwyrm import settings
|
from bookwyrm import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def download_file(url, destination):
|
||||||
|
"""Downloads a file to the given path"""
|
||||||
|
try:
|
||||||
|
# Ensure our destination directory exists
|
||||||
|
os.makedirs(os.path.dirname(destination))
|
||||||
|
with urllib.request.urlopen(url) as stream:
|
||||||
|
with open(destination, "b+w") as outfile:
|
||||||
|
outfile.write(stream.read())
|
||||||
|
except (urllib.error.HTTPError, urllib.error.URLError):
|
||||||
|
logger.error("Failed to download file %s", url)
|
||||||
|
except OSError:
|
||||||
|
logger.error("Couldn't open font file %s for writing", destination)
|
||||||
|
except: # pylint: disable=bare-except
|
||||||
|
logger.exception("Unknown error in file download")
|
||||||
|
|
||||||
|
|
||||||
class BookwyrmConfig(AppConfig):
|
class BookwyrmConfig(AppConfig):
|
||||||
|
"""Handles additional configuration"""
|
||||||
|
|
||||||
name = "bookwyrm"
|
name = "bookwyrm"
|
||||||
verbose_name = "BookWyrm"
|
verbose_name = "BookWyrm"
|
||||||
|
|
||||||
|
@ -11,3 +37,15 @@ class BookwyrmConfig(AppConfig):
|
||||||
from bookwyrm.telemetry import open_telemetry
|
from bookwyrm.telemetry import open_telemetry
|
||||||
|
|
||||||
open_telemetry.instrumentDjango()
|
open_telemetry.instrumentDjango()
|
||||||
|
|
||||||
|
if settings.ENABLE_PREVIEW_IMAGES and settings.FONTS:
|
||||||
|
# Download any fonts that we don't have yet
|
||||||
|
logger.debug("Downloading fonts..")
|
||||||
|
for name, config in settings.FONTS.items():
|
||||||
|
font_path = os.path.join(
|
||||||
|
settings.FONT_DIR, config["directory"], config["filename"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if "url" in config and not os.path.exists(font_path):
|
||||||
|
logger.info("Just a sec, downloading %s", name)
|
||||||
|
download_file(config["url"], font_path)
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
""" functionality outline for a book data connector """
|
""" functionality outline for a book data connector """
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
import imghdr
|
||||||
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
import requests
|
import requests
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
|
@ -248,6 +252,8 @@ def dict_from_mappings(data, mappings):
|
||||||
def get_data(url, params=None, timeout=10):
|
def get_data(url, params=None, timeout=10):
|
||||||
"""wrapper for request.get"""
|
"""wrapper for request.get"""
|
||||||
# check if the url is blocked
|
# check if the url is blocked
|
||||||
|
raise_not_valid_url(url)
|
||||||
|
|
||||||
if models.FederatedServer.is_blocked(url):
|
if models.FederatedServer.is_blocked(url):
|
||||||
raise ConnectorException(f"Attempting to load data from blocked url: {url}")
|
raise ConnectorException(f"Attempting to load data from blocked url: {url}")
|
||||||
|
|
||||||
|
@ -280,6 +286,7 @@ def get_data(url, params=None, timeout=10):
|
||||||
|
|
||||||
def get_image(url, timeout=10):
|
def get_image(url, timeout=10):
|
||||||
"""wrapper for requesting an image"""
|
"""wrapper for requesting an image"""
|
||||||
|
raise_not_valid_url(url)
|
||||||
try:
|
try:
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
url,
|
url,
|
||||||
|
@ -290,10 +297,32 @@ def get_image(url, timeout=10):
|
||||||
)
|
)
|
||||||
except RequestException as err:
|
except RequestException as err:
|
||||||
logger.exception(err)
|
logger.exception(err)
|
||||||
return None
|
return None, None
|
||||||
|
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
return None
|
return None, None
|
||||||
return resp
|
|
||||||
|
image_content = ContentFile(resp.content)
|
||||||
|
extension = imghdr.what(None, image_content.read())
|
||||||
|
if not extension:
|
||||||
|
logger.exception("File requested was not an image: %s", url)
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
return image_content, extension
|
||||||
|
|
||||||
|
|
||||||
|
def raise_not_valid_url(url):
|
||||||
|
"""do some basic reality checks on the url"""
|
||||||
|
parsed = urlparse(url)
|
||||||
|
if not parsed.scheme in ["http", "https"]:
|
||||||
|
raise ConnectorException("Invalid scheme: ", url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ipaddress.ip_address(parsed.netloc)
|
||||||
|
raise ConnectorException("Provided url is an IP address: ", url)
|
||||||
|
except ValueError:
|
||||||
|
# it's not an IP address, which is good
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Mapping:
|
class Mapping:
|
||||||
|
|
|
@ -48,7 +48,9 @@ def moderation_report_email(report):
|
||||||
data["reportee"] = report.user.localname or report.user.username
|
data["reportee"] = report.user.localname or report.user.username
|
||||||
data["report_link"] = report.remote_id
|
data["report_link"] = report.remote_id
|
||||||
|
|
||||||
for admin in models.User.objects.filter(groups__name__in=["admin", "moderator"]):
|
for admin in models.User.objects.filter(
|
||||||
|
groups__name__in=["admin", "moderator"]
|
||||||
|
).distinct():
|
||||||
data["user"] = admin.display_name
|
data["user"] = admin.display_name
|
||||||
send_email.delay(admin.email, *format_email("moderation_report", data))
|
send_email.delay(admin.email, *format_email("moderation_report", data))
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
""" using django model forms """
|
""" using django model forms """
|
||||||
import datetime
|
import datetime
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms import ModelForm, PasswordInput, widgets, ChoiceField
|
from django.forms import ModelForm, PasswordInput, widgets, ChoiceField
|
||||||
|
@ -227,6 +228,34 @@ class FileLinkForm(CustomForm):
|
||||||
model = models.FileLink
|
model = models.FileLink
|
||||||
fields = ["url", "filetype", "availability", "book", "added_by"]
|
fields = ["url", "filetype", "availability", "book", "added_by"]
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
"""make sure the domain isn't blocked or pending"""
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
url = cleaned_data.get("url")
|
||||||
|
filetype = cleaned_data.get("filetype")
|
||||||
|
book = cleaned_data.get("book")
|
||||||
|
domain = urlparse(url).netloc
|
||||||
|
if models.LinkDomain.objects.filter(domain=domain).exists():
|
||||||
|
status = models.LinkDomain.objects.get(domain=domain).status
|
||||||
|
if status == "blocked":
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
self.add_error(
|
||||||
|
"url",
|
||||||
|
_(
|
||||||
|
"This domain is blocked. Please contact your administrator if you think this is an error."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
elif models.FileLink.objects.filter(
|
||||||
|
url=url, book=book, filetype=filetype
|
||||||
|
).exists():
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
self.add_error(
|
||||||
|
"url",
|
||||||
|
_(
|
||||||
|
"This link with file type has already been added for this book. If it is not visible, the domain is still pending."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EditionForm(CustomForm):
|
class EditionForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -444,6 +473,12 @@ class ListForm(CustomForm):
|
||||||
fields = ["user", "name", "description", "curation", "privacy", "group"]
|
fields = ["user", "name", "description", "curation", "privacy", "group"]
|
||||||
|
|
||||||
|
|
||||||
|
class ListItemForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.ListItem
|
||||||
|
fields = ["user", "book", "book_list", "notes"]
|
||||||
|
|
||||||
|
|
||||||
class GroupForm(CustomForm):
|
class GroupForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Group
|
model = models.Group
|
||||||
|
@ -500,7 +535,7 @@ class ReadThroughForm(CustomForm):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
start_date = cleaned_data.get("start_date")
|
start_date = cleaned_data.get("start_date")
|
||||||
finish_date = cleaned_data.get("finish_date")
|
finish_date = cleaned_data.get("finish_date")
|
||||||
if start_date > finish_date:
|
if start_date and finish_date and start_date > finish_date:
|
||||||
self.add_error(
|
self.add_error(
|
||||||
"finish_date", _("Reading finish date cannot be before start date.")
|
"finish_date", _("Reading finish date cannot be before start date.")
|
||||||
)
|
)
|
||||||
|
|
|
@ -19,9 +19,7 @@ def init_permissions():
|
||||||
{
|
{
|
||||||
"codename": "edit_instance_settings",
|
"codename": "edit_instance_settings",
|
||||||
"name": "change the instance info",
|
"name": "change the instance info",
|
||||||
"groups": [
|
"groups": ["admin"],
|
||||||
"admin",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"codename": "set_user_group",
|
"codename": "set_user_group",
|
||||||
|
@ -55,7 +53,7 @@ def init_permissions():
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
content_type = models.ContentType.objects.get_for_model(User)
|
content_type = ContentType.objects.get_for_model(models.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"],
|
||||||
|
@ -66,15 +64,12 @@ def init_permissions():
|
||||||
for group_name in permission["groups"]:
|
for group_name in permission["groups"]:
|
||||||
Group.objects.get(name=group_name).permissions.add(permission_obj)
|
Group.objects.get(name=group_name).permissions.add(permission_obj)
|
||||||
|
|
||||||
# while the groups and permissions shouldn't be changed because the code
|
|
||||||
# depends on them, what permissions go with what groups should be editable
|
|
||||||
|
|
||||||
|
|
||||||
def init_connectors():
|
def init_connectors():
|
||||||
"""access book data sources"""
|
"""access book data sources"""
|
||||||
models.Connector.objects.create(
|
models.Connector.objects.create(
|
||||||
identifier="bookwyrm.social",
|
identifier="bookwyrm.social",
|
||||||
name="BookWyrm dot Social",
|
name="Bookwyrm.social",
|
||||||
connector_file="bookwyrm_connector",
|
connector_file="bookwyrm_connector",
|
||||||
base_url="https://bookwyrm.social",
|
base_url="https://bookwyrm.social",
|
||||||
books_url="https://bookwyrm.social/book",
|
books_url="https://bookwyrm.social/book",
|
||||||
|
@ -84,6 +79,7 @@ def init_connectors():
|
||||||
priority=2,
|
priority=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# pylint: disable=line-too-long
|
||||||
models.Connector.objects.create(
|
models.Connector.objects.create(
|
||||||
identifier="inventaire.io",
|
identifier="inventaire.io",
|
||||||
name="Inventaire",
|
name="Inventaire",
|
||||||
|
@ -127,7 +123,7 @@ def init_settings():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def init_link_domains(*_):
|
def init_link_domains():
|
||||||
"""safe book links"""
|
"""safe book links"""
|
||||||
domains = [
|
domains = [
|
||||||
("standardebooks.org", "Standard EBooks"),
|
("standardebooks.org", "Standard EBooks"),
|
||||||
|
@ -144,10 +140,15 @@ def init_link_domains(*_):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
# pylint: disable=unused-argument
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
"""command-line options"""
|
||||||
|
|
||||||
help = "Initializes the database with starter data"
|
help = "Initializes the database with starter data"
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
|
"""specify which function to run"""
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--limit",
|
"--limit",
|
||||||
default=None,
|
default=None,
|
||||||
|
@ -155,6 +156,7 @@ class Command(BaseCommand):
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
|
"""execute init"""
|
||||||
limit = options.get("limit")
|
limit = options.get("limit")
|
||||||
tables = [
|
tables = [
|
||||||
"group",
|
"group",
|
||||||
|
@ -164,7 +166,7 @@ class Command(BaseCommand):
|
||||||
"settings",
|
"settings",
|
||||||
"linkdomain",
|
"linkdomain",
|
||||||
]
|
]
|
||||||
if limit not in tables:
|
if limit and limit not in tables:
|
||||||
raise Exception("Invalid table limit:", limit)
|
raise Exception("Invalid table limit:", limit)
|
||||||
|
|
||||||
if not limit or limit == "group":
|
if not limit or limit == "group":
|
||||||
|
|
21
bookwyrm/migrations/0130_alter_listitem_notes.py
Normal file
21
bookwyrm/migrations/0130_alter_listitem_notes.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 3.2.10 on 2022-01-24 20:01
|
||||||
|
|
||||||
|
import bookwyrm.models.fields
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0129_auto_20220117_1716"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="listitem",
|
||||||
|
name="notes",
|
||||||
|
field=bookwyrm.models.fields.TextField(
|
||||||
|
blank=True, max_length=300, null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
37
bookwyrm/migrations/0130_alter_user_preferred_language.py
Normal file
37
bookwyrm/migrations/0130_alter_user_preferred_language.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Generated by Django 3.2.10 on 2022-01-24 17:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0129_auto_20220117_1716"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="preferred_language",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("en-us", "English"),
|
||||||
|
("de-de", "Deutsch (German)"),
|
||||||
|
("es-es", "Español (Spanish)"),
|
||||||
|
("gl-es", "Galego (Galician)"),
|
||||||
|
("it-it", "Italiano (Italian)"),
|
||||||
|
("fr-fr", "Français (French)"),
|
||||||
|
("lt-lt", "Lietuvių (Lithuanian)"),
|
||||||
|
("no-no", "Norsk (Norwegian)"),
|
||||||
|
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
||||||
|
("pt-pt", "Português Europeu (European Portuguese)"),
|
||||||
|
("sv-se", "Swedish (Svenska)"),
|
||||||
|
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||||
|
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
13
bookwyrm/migrations/0131_merge_20220125_1644.py
Normal file
13
bookwyrm/migrations/0131_merge_20220125_1644.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 3.2.10 on 2022-01-25 16:44
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0130_alter_listitem_notes"),
|
||||||
|
("bookwyrm", "0130_alter_user_preferred_language"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
37
bookwyrm/migrations/0132_alter_user_preferred_language.py
Normal file
37
bookwyrm/migrations/0132_alter_user_preferred_language.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Generated by Django 3.2.10 on 2022-02-02 20:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0131_merge_20220125_1644"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="preferred_language",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("en-us", "English"),
|
||||||
|
("de-de", "Deutsch (German)"),
|
||||||
|
("es-es", "Español (Spanish)"),
|
||||||
|
("gl-es", "Galego (Galician)"),
|
||||||
|
("it-it", "Italiano (Italian)"),
|
||||||
|
("fr-fr", "Français (French)"),
|
||||||
|
("lt-lt", "Lietuvių (Lithuanian)"),
|
||||||
|
("no-no", "Norsk (Norwegian)"),
|
||||||
|
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
||||||
|
("pt-pt", "Português Europeu (European Portuguese)"),
|
||||||
|
("sv-se", "Svenska (Swedish)"),
|
||||||
|
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||||
|
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
21
bookwyrm/migrations/0133_alter_listitem_notes.py
Normal file
21
bookwyrm/migrations/0133_alter_listitem_notes.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 3.2.11 on 2022-02-04 20:06
|
||||||
|
|
||||||
|
import bookwyrm.models.fields
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0132_alter_user_preferred_language"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="listitem",
|
||||||
|
name="notes",
|
||||||
|
field=bookwyrm.models.fields.HtmlField(
|
||||||
|
blank=True, max_length=300, null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -342,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
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
""" activitypub-aware django model fields """
|
""" activitypub-aware django model fields """
|
||||||
from dataclasses import MISSING
|
from dataclasses import MISSING
|
||||||
import imghdr
|
|
||||||
import re
|
import re
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
@ -9,7 +8,6 @@ import dateutil.parser
|
||||||
from dateutil.parser import ParserError
|
from dateutil.parser import ParserError
|
||||||
from django.contrib.postgres.fields import ArrayField as DjangoArrayField
|
from django.contrib.postgres.fields import ArrayField as DjangoArrayField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.files.base import ContentFile
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.forms import ClearableFileInput, ImageField as DjangoImageField
|
from django.forms import ClearableFileInput, ImageField as DjangoImageField
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -443,12 +441,10 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
response = get_image(url)
|
image_content, extension = get_image(url)
|
||||||
if not response:
|
if not image_content:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
image_content = ContentFile(response.content)
|
|
||||||
extension = imghdr.what(None, image_content.read()) or ""
|
|
||||||
image_name = f"{uuid4()}.{extension}"
|
image_name = f"{uuid4()}.{extension}"
|
||||||
return [image_name, image_content]
|
return [image_name, image_content]
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -74,6 +75,22 @@ class List(OrderedCollectionMixin, BookWyrmModel):
|
||||||
return
|
return
|
||||||
super().raise_not_editable(viewer)
|
super().raise_not_editable(viewer)
|
||||||
|
|
||||||
|
def raise_not_submittable(self, viewer):
|
||||||
|
"""can the user submit a book to the list?"""
|
||||||
|
# if you can't view the list you can't submit to it
|
||||||
|
self.raise_visible_to_user(viewer)
|
||||||
|
|
||||||
|
# all good if you're the owner or the list is open
|
||||||
|
if self.user == viewer or self.curation in ["open", "curated"]:
|
||||||
|
return
|
||||||
|
if self.curation == "group":
|
||||||
|
is_group_member = GroupMember.objects.filter(
|
||||||
|
group=self.group, user=viewer
|
||||||
|
).exists()
|
||||||
|
if is_group_member:
|
||||||
|
return
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def followers_filter(cls, queryset, viewer):
|
def followers_filter(cls, queryset, viewer):
|
||||||
"""Override filter for "followers" privacy level to allow non-following
|
"""Override filter for "followers" privacy level to allow non-following
|
||||||
|
@ -125,7 +142,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
|
||||||
user = fields.ForeignKey(
|
user = fields.ForeignKey(
|
||||||
"User", on_delete=models.PROTECT, activitypub_field="actor"
|
"User", on_delete=models.PROTECT, activitypub_field="actor"
|
||||||
)
|
)
|
||||||
notes = fields.TextField(blank=True, null=True)
|
notes = fields.HtmlField(blank=True, null=True, max_length=300)
|
||||||
approved = models.BooleanField(default=True)
|
approved = models.BooleanField(default=True)
|
||||||
order = fields.IntegerField()
|
order = fields.IntegerField()
|
||||||
endorsement = models.ManyToManyField("User", related_name="endorsers")
|
endorsement = models.ManyToManyField("User", related_name="endorsers")
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
""" flagged for moderation """
|
""" flagged for moderation """
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from bookwyrm.settings import DOMAIN
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,6 +16,9 @@ class Report(BookWyrmModel):
|
||||||
links = models.ManyToManyField("Link", blank=True)
|
links = models.ManyToManyField("Link", blank=True)
|
||||||
resolved = models.BooleanField(default=False)
|
resolved = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def get_remote_id(self):
|
||||||
|
return f"https://{DOMAIN}/settings/reports/{self.id}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""set order by default"""
|
"""set order by default"""
|
||||||
|
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -4,6 +4,7 @@ import os
|
||||||
import textwrap
|
import textwrap
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
import logging
|
||||||
|
|
||||||
import colorsys
|
import colorsys
|
||||||
from colorthief import ColorThief
|
from colorthief import ColorThief
|
||||||
|
@ -17,34 +18,49 @@ from django.db.models import Avg
|
||||||
from bookwyrm import models, settings
|
from bookwyrm import models, settings
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
IMG_WIDTH = settings.PREVIEW_IMG_WIDTH
|
IMG_WIDTH = settings.PREVIEW_IMG_WIDTH
|
||||||
IMG_HEIGHT = settings.PREVIEW_IMG_HEIGHT
|
IMG_HEIGHT = settings.PREVIEW_IMG_HEIGHT
|
||||||
BG_COLOR = settings.PREVIEW_BG_COLOR
|
BG_COLOR = settings.PREVIEW_BG_COLOR
|
||||||
TEXT_COLOR = settings.PREVIEW_TEXT_COLOR
|
TEXT_COLOR = settings.PREVIEW_TEXT_COLOR
|
||||||
DEFAULT_COVER_COLOR = settings.PREVIEW_DEFAULT_COVER_COLOR
|
DEFAULT_COVER_COLOR = settings.PREVIEW_DEFAULT_COVER_COLOR
|
||||||
|
DEFAULT_FONT = settings.PREVIEW_DEFAULT_FONT
|
||||||
TRANSPARENT_COLOR = (0, 0, 0, 0)
|
TRANSPARENT_COLOR = (0, 0, 0, 0)
|
||||||
|
|
||||||
margin = math.floor(IMG_HEIGHT / 10)
|
margin = math.floor(IMG_HEIGHT / 10)
|
||||||
gutter = math.floor(margin / 2)
|
gutter = math.floor(margin / 2)
|
||||||
inner_img_height = math.floor(IMG_HEIGHT * 0.8)
|
inner_img_height = math.floor(IMG_HEIGHT * 0.8)
|
||||||
inner_img_width = math.floor(inner_img_height * 0.7)
|
inner_img_width = math.floor(inner_img_height * 0.7)
|
||||||
font_dir = os.path.join(settings.STATIC_ROOT, "fonts/public_sans")
|
|
||||||
|
|
||||||
|
|
||||||
def get_font(font_name, size=28):
|
def get_imagefont(name, size):
|
||||||
"""Loads custom font"""
|
"""Loads an ImageFont based on config"""
|
||||||
if font_name == "light":
|
try:
|
||||||
font_path = os.path.join(font_dir, "PublicSans-Light.ttf")
|
config = settings.FONTS[name]
|
||||||
if font_name == "regular":
|
path = os.path.join(settings.FONT_DIR, config["directory"], config["filename"])
|
||||||
font_path = os.path.join(font_dir, "PublicSans-Regular.ttf")
|
return ImageFont.truetype(path, size)
|
||||||
elif font_name == "bold":
|
except KeyError:
|
||||||
font_path = os.path.join(font_dir, "PublicSans-Bold.ttf")
|
logger.error("Font %s not found in config", name)
|
||||||
|
except OSError:
|
||||||
|
logger.error("Could not load font %s from file", name)
|
||||||
|
|
||||||
|
return ImageFont.load_default()
|
||||||
|
|
||||||
|
|
||||||
|
def get_font(weight, size=28):
|
||||||
|
"""Gets a custom font with the given weight and size"""
|
||||||
|
font = get_imagefont(DEFAULT_FONT, size)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(font_path, size)
|
if weight == "light":
|
||||||
except OSError:
|
font.set_variation_by_name("Light")
|
||||||
font = ImageFont.load_default()
|
if weight == "bold":
|
||||||
|
font.set_variation_by_name("Bold")
|
||||||
|
if weight == "regular":
|
||||||
|
font.set_variation_by_name("Regular")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
return font
|
return font
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
|
||||||
"ol",
|
"ol",
|
||||||
"li",
|
"li",
|
||||||
]
|
]
|
||||||
|
self.allowed_attrs = ["href", "rel", "src", "alt"]
|
||||||
self.tag_stack = []
|
self.tag_stack = []
|
||||||
self.output = []
|
self.output = []
|
||||||
# if the html appears invalid, we just won't allow any at all
|
# if the html appears invalid, we just won't allow any at all
|
||||||
|
@ -30,7 +31,14 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
|
||||||
def handle_starttag(self, tag, attrs):
|
def handle_starttag(self, tag, attrs):
|
||||||
"""check if the tag is valid"""
|
"""check if the tag is valid"""
|
||||||
if self.allow_html and tag in self.allowed_tags:
|
if self.allow_html and tag in self.allowed_tags:
|
||||||
self.output.append(("tag", self.get_starttag_text()))
|
allowed_attrs = " ".join(
|
||||||
|
f'{a}="{v}"' for a, v in attrs if a in self.allowed_attrs
|
||||||
|
)
|
||||||
|
reconstructed = f"<{tag}"
|
||||||
|
if allowed_attrs:
|
||||||
|
reconstructed += " " + allowed_attrs
|
||||||
|
reconstructed += ">"
|
||||||
|
self.output.append(("tag", reconstructed))
|
||||||
self.tag_stack.append(tag)
|
self.tag_stack.append(tag)
|
||||||
else:
|
else:
|
||||||
self.output.append(("data", ""))
|
self.output.append(("data", ""))
|
||||||
|
|
|
@ -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.2.0"
|
VERSION = "0.3.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 = "76c5ff1f"
|
JS_CACHE = "7b5303af"
|
||||||
|
|
||||||
# email
|
# email
|
||||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||||
|
@ -35,6 +35,9 @@ LOCALE_PATHS = [
|
||||||
]
|
]
|
||||||
LANGUAGE_COOKIE_NAME = env.str("LANGUAGE_COOKIE_NAME", "django_language")
|
LANGUAGE_COOKIE_NAME = env.str("LANGUAGE_COOKIE_NAME", "django_language")
|
||||||
|
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||||
|
|
||||||
# Preview image
|
# Preview image
|
||||||
|
@ -44,6 +47,17 @@ PREVIEW_TEXT_COLOR = env.str("PREVIEW_TEXT_COLOR", "#363636")
|
||||||
PREVIEW_IMG_WIDTH = env.int("PREVIEW_IMG_WIDTH", 1200)
|
PREVIEW_IMG_WIDTH = env.int("PREVIEW_IMG_WIDTH", 1200)
|
||||||
PREVIEW_IMG_HEIGHT = env.int("PREVIEW_IMG_HEIGHT", 630)
|
PREVIEW_IMG_HEIGHT = env.int("PREVIEW_IMG_HEIGHT", 630)
|
||||||
PREVIEW_DEFAULT_COVER_COLOR = env.str("PREVIEW_DEFAULT_COVER_COLOR", "#002549")
|
PREVIEW_DEFAULT_COVER_COLOR = env.str("PREVIEW_DEFAULT_COVER_COLOR", "#002549")
|
||||||
|
PREVIEW_DEFAULT_FONT = env.str("PREVIEW_DEFAULT_FONT", "Source Han Sans")
|
||||||
|
|
||||||
|
FONTS = {
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
"Source Han Sans": {
|
||||||
|
"directory": "source_han_sans",
|
||||||
|
"filename": "SourceHanSans-VF.ttf.ttc",
|
||||||
|
"url": "https://github.com/adobe-fonts/source-han-sans/raw/release/Variable/OTC/SourceHanSans-VF.ttf.ttc",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FONT_DIR = os.path.join(STATIC_ROOT, "fonts")
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||||
|
@ -106,6 +120,61 @@ 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,
|
||||||
|
},
|
||||||
|
"django.utils.autoreload": {
|
||||||
|
"level": "INFO",
|
||||||
|
},
|
||||||
|
# Add a bookwyrm-specific logger
|
||||||
|
"bookwyrm": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": LOG_LEVEL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
WSGI_APPLICATION = "bookwyrm.wsgi.application"
|
WSGI_APPLICATION = "bookwyrm.wsgi.application"
|
||||||
|
|
||||||
|
@ -196,7 +265,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = "en-us"
|
LANGUAGE_CODE = env("LANGUAGE_CODE", "en-us")
|
||||||
LANGUAGES = [
|
LANGUAGES = [
|
||||||
("en-us", _("English")),
|
("en-us", _("English")),
|
||||||
("de-de", _("Deutsch (German)")),
|
("de-de", _("Deutsch (German)")),
|
||||||
|
@ -208,6 +277,7 @@ LANGUAGES = [
|
||||||
("no-no", _("Norsk (Norwegian)")),
|
("no-no", _("Norsk (Norwegian)")),
|
||||||
("pt-br", _("Português do Brasil (Brazilian Portuguese)")),
|
("pt-br", _("Português do Brasil (Brazilian Portuguese)")),
|
||||||
("pt-pt", _("Português Europeu (European Portuguese)")),
|
("pt-pt", _("Português Europeu (European Portuguese)")),
|
||||||
|
("sv-se", _("Svenska (Swedish)")),
|
||||||
("zh-hans", _("简体中文 (Simplified Chinese)")),
|
("zh-hans", _("简体中文 (Simplified Chinese)")),
|
||||||
("zh-hant", _("繁體中文 (Traditional Chinese)")),
|
("zh-hant", _("繁體中文 (Traditional Chinese)")),
|
||||||
]
|
]
|
||||||
|
@ -263,16 +333,11 @@ if USE_S3:
|
||||||
MEDIA_FULL_URL = MEDIA_URL
|
MEDIA_FULL_URL = MEDIA_URL
|
||||||
STATIC_FULL_URL = STATIC_URL
|
STATIC_FULL_URL = STATIC_URL
|
||||||
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
|
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
|
||||||
# I don't know if it's used, but the site crashes without it
|
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
|
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
|
|
||||||
else:
|
else:
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
|
|
||||||
MEDIA_URL = "/images/"
|
MEDIA_URL = "/images/"
|
||||||
MEDIA_FULL_URL = f"{PROTOCOL}://{DOMAIN}{MEDIA_URL}"
|
MEDIA_FULL_URL = f"{PROTOCOL}://{DOMAIN}{MEDIA_URL}"
|
||||||
STATIC_FULL_URL = f"{PROTOCOL}://{DOMAIN}{STATIC_URL}"
|
STATIC_FULL_URL = f"{PROTOCOL}://{DOMAIN}{STATIC_URL}"
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
|
|
||||||
|
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT = env("OTEL_EXPORTER_OTLP_ENDPOINT", None)
|
OTEL_EXPORTER_OTLP_ENDPOINT = env("OTEL_EXPORTER_OTLP_ENDPOINT", None)
|
||||||
OTEL_EXPORTER_OTLP_HEADERS = env("OTEL_EXPORTER_OTLP_HEADERS", None)
|
OTEL_EXPORTER_OTLP_HEADERS = env("OTEL_EXPORTER_OTLP_HEADERS", None)
|
||||||
|
|
96
bookwyrm/static/fonts/source_han_sans/LICENSE.txt
Normal file
96
bookwyrm/static/fonts/source_han_sans/LICENSE.txt
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font
|
||||||
|
Name 'Source'. Source is a trademark of Adobe in the United States
|
||||||
|
and/or other countries.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License,
|
||||||
|
Version 1.1.
|
||||||
|
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font
|
||||||
|
creation efforts of academic and linguistic communities, and to
|
||||||
|
provide a free and open framework in which fonts may be shared and
|
||||||
|
improved in partnership with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply to
|
||||||
|
any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software
|
||||||
|
components as distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to,
|
||||||
|
deleting, or substituting -- in part or in whole -- any of the
|
||||||
|
components of the Original Version, by changing formats or by porting
|
||||||
|
the Font Software to a new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||||
|
modify, redistribute, and sell modified and unmodified copies of the
|
||||||
|
Font Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components, in
|
||||||
|
Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the
|
||||||
|
corresponding Copyright Holder. This restriction only applies to the
|
||||||
|
primary font name as presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created using
|
||||||
|
the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
9
bookwyrm/static/fonts/source_han_sans/README.txt
Normal file
9
bookwyrm/static/fonts/source_han_sans/README.txt
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
The font file itself is not included in the Git repository to avoid putting
|
||||||
|
large files in the repo history. The Docker image should download the correct
|
||||||
|
font into this folder automatically.
|
||||||
|
|
||||||
|
In case something goes wrong, the font used is the Variable OTC TTF, available
|
||||||
|
as of this writing from the Adobe Fonts GitHub repository:
|
||||||
|
https://github.com/adobe-fonts/source-han-sans/tree/release#user-content-variable-otcs
|
||||||
|
|
||||||
|
BookWyrm expects the file to be in this folder, named SourceHanSans-VF.ttf.ttc
|
|
@ -122,39 +122,13 @@ let BookWyrm = new (class {
|
||||||
*/
|
*/
|
||||||
updateCountElement(counter, data) {
|
updateCountElement(counter, data) {
|
||||||
let count = data.count;
|
let count = data.count;
|
||||||
const count_by_type = data.count_by_type;
|
|
||||||
|
if (count === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentCount = counter.innerText;
|
const currentCount = counter.innerText;
|
||||||
const hasMentions = data.has_mentions;
|
const hasMentions = data.has_mentions;
|
||||||
const allowedStatusTypesEl = document.getElementById("unread-notifications-wrapper");
|
|
||||||
|
|
||||||
// If we're on the right counter element
|
|
||||||
if (counter.closest("[data-poll-wrapper]").contains(allowedStatusTypesEl)) {
|
|
||||||
const allowedStatusTypes = JSON.parse(allowedStatusTypesEl.textContent);
|
|
||||||
|
|
||||||
// For keys in common between allowedStatusTypes and count_by_type
|
|
||||||
// This concerns 'review', 'quotation', 'comment'
|
|
||||||
count = allowedStatusTypes.reduce(function (prev, currentKey) {
|
|
||||||
const currentValue = count_by_type[currentKey] | 0;
|
|
||||||
|
|
||||||
return prev + currentValue;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// Add all the "other" in count_by_type if 'everything' is allowed
|
|
||||||
if (allowedStatusTypes.includes("everything")) {
|
|
||||||
// Clone count_by_type with 0 for reviews/quotations/comments
|
|
||||||
const count_by_everything_else = Object.assign({}, count_by_type, {
|
|
||||||
review: 0,
|
|
||||||
quotation: 0,
|
|
||||||
comment: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
count = Object.keys(count_by_everything_else).reduce(function (prev, currentKey) {
|
|
||||||
const currentValue = count_by_everything_else[currentKey] | 0;
|
|
||||||
|
|
||||||
return prev + currentValue;
|
|
||||||
}, count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count != currentCount) {
|
if (count != currentCount) {
|
||||||
this.addRemoveClass(counter.closest("[data-poll-wrapper]"), "is-hidden", count < 1);
|
this.addRemoveClass(counter.closest("[data-poll-wrapper]"), "is-hidden", count < 1);
|
||||||
|
@ -517,7 +491,7 @@ let BookWyrm = new (class {
|
||||||
|
|
||||||
duplicateInput(event) {
|
duplicateInput(event) {
|
||||||
const trigger = event.currentTarget;
|
const trigger = event.currentTarget;
|
||||||
const input_id = trigger.dataset["duplicate"];
|
const input_id = trigger.dataset.duplicate;
|
||||||
const orig = document.getElementById(input_id);
|
const orig = document.getElementById(input_id);
|
||||||
const parent = orig.parentNode;
|
const parent = orig.parentNode;
|
||||||
const new_count = parent.querySelectorAll("input").length + 1;
|
const new_count = parent.querySelectorAll("input").length + 1;
|
||||||
|
|
|
@ -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,8 +27,8 @@
|
||||||
</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=superlatives.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">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -66,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>
|
||||||
|
@ -74,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>
|
||||||
|
@ -83,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 %}
|
||||||
|
@ -98,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>
|
||||||
|
|
||||||
|
@ -114,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>
|
||||||
|
@ -122,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>
|
||||||
|
@ -141,12 +141,14 @@
|
||||||
<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>
|
</div>
|
||||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
@ -356,10 +356,11 @@
|
||||||
<form name="list-add" method="post" action="{% url 'list-add-book' %}">
|
<form name="list-add" method="post" action="{% url 'list-add-book' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<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 }}">
|
||||||
<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 is-clipped">
|
<div class="select control is-clipped">
|
||||||
<select name="list" id="id_list">
|
<select name="book_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>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -2,15 +2,17 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block filter %}
|
{% block filter %}
|
||||||
<label class="label is-block" for="id_format">{% trans "Format:" %}</label>
|
<div class="control">
|
||||||
<div class="select">
|
<label class="label is-block" for="id_format">{% trans "Format:" %}</label>
|
||||||
<select id="id_format" name="format">
|
<div class="select">
|
||||||
<option value="">{% trans "Any" %}</option>
|
<select id="id_format" name="format">
|
||||||
{% for format in formats %}{% if format %}
|
<option value="">{% trans "Any" %}</option>
|
||||||
<option value="{{ format }}" {% if request.GET.format == format %}selected{% endif %}>
|
{% for format in formats %}{% if format %}
|
||||||
{{ format|title }}
|
<option value="{{ format }}" {% if request.GET.format == format %}selected{% endif %}>
|
||||||
</option>
|
{{ format|title }}
|
||||||
{% endif %}{% endfor %}
|
</option>
|
||||||
</select>
|
{% endif %}{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -2,15 +2,17 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block filter %}
|
{% block filter %}
|
||||||
<label class="label is-block" for="id_language">{% trans "Language:" %}</label>
|
<div class="control">
|
||||||
<div class="select">
|
<label class="label is-block" for="id_language">{% trans "Language:" %}</label>
|
||||||
<select id="id_language" name="language">
|
<div class="select">
|
||||||
<option value="">{% trans "Any" %}</option>
|
<select id="id_language" name="language">
|
||||||
{% for language in languages %}
|
<option value="">{% trans "Any" %}</option>
|
||||||
<option value="{{ language }}" {% if request.GET.language == language %}selected{% endif %}>
|
{% for language in languages %}
|
||||||
{{ language }}
|
<option value="{{ language }}" {% if request.GET.language == language %}selected{% endif %}>
|
||||||
</option>
|
{{ language }}
|
||||||
{% endfor %}
|
</option>
|
||||||
</select>
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block filter %}
|
{% block filter %}
|
||||||
<label class="label" for="id_search">{% trans "Search editions" %}</label>
|
<div class="control">
|
||||||
<input type="text" class="input" name="q" value="{{ request.GET.q|default:'' }}" id="id_search">
|
<label class="label" for="id_search">{% trans "Search editions" %}</label>
|
||||||
|
<input type="text" class="input" name="q" value="{{ request.GET.q|default:'' }}" id="id_search">
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -56,9 +56,7 @@
|
||||||
|
|
||||||
{% block modal-footer %}
|
{% block modal-footer %}
|
||||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||||
{% if not static %}
|
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block modal-form-close %}</form>{% endblock %}
|
{% block modal-form-close %}</form>{% endblock %}
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
{% for link in links %}
|
{% for link in links %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="overflow-wrap-anywhere">
|
<td class="overflow-wrap-anywhere">
|
||||||
<a href="{{ link.url }}" target="_blank" rel="noopener">{{ link.url }}</a>
|
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer">{{ link.url }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'user-feed' link.added_by.id %}">{{ link.added_by.display_name }}</a>
|
<a href="{% url 'user-feed' link.added_by.id %}">{{ link.added_by.display_name }}</a>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load book_display_tags %}
|
||||||
{% load utilities %}
|
{% load utilities %}
|
||||||
|
|
||||||
{% get_book_file_links book as links %}
|
{% get_book_file_links book as links %}
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
{% for link in links.all %}
|
{% for link in links.all %}
|
||||||
{% join "verify" link.id as verify_modal %}
|
{% join "verify" link.id as verify_modal %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ link.url }}" rel="noopener" target="_blank" title="{{ link.url }}" data-modal-open="{{ verify_modal }}">{{ link.name }}</a>
|
<a href="{{ link.url }}" rel="noopener noreferrer" target="_blank" title="{{ link.url }}" data-modal-open="{{ verify_modal }}">{{ link.name }}</a>
|
||||||
({{ link.filetype }})
|
({{ link.filetype }})
|
||||||
|
|
||||||
{% if link.availability != "free" %}
|
{% if link.availability != "free" %}
|
||||||
|
|
|
@ -17,7 +17,7 @@ Is that where you'd like to go?
|
||||||
|
|
||||||
|
|
||||||
{% block modal-footer %}
|
{% block modal-footer %}
|
||||||
<a href="{{ link.url }}" target="_blank" rel="noopener" class="button is-primary">{% trans "Continue" %}</a>
|
<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>
|
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||||
|
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load rating_tags %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load utilities %}
|
{% load utilities %}
|
||||||
{% load status_display %}
|
{% load status_display %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -24,9 +24,12 @@
|
||||||
|
|
||||||
{# announcements and system messages #}
|
{# announcements and system messages #}
|
||||||
{% if not activities.number > 1 %}
|
{% if not activities.number > 1 %}
|
||||||
<a href="{{ request.path }}" class="transition-y is-hidden notification is-primary is-block" data-poll-wrapper>
|
<a
|
||||||
{% blocktrans with tab_key=tab.key %}load <span data-poll="stream/{{ tab_key }}">0</span> unread status(es){% endblocktrans %}
|
href="{{ request.path }}"
|
||||||
{{ allowed_status_types|json_script:"unread-notifications-wrapper" }}
|
class="transition-y is-hidden notification is-primary is-block"
|
||||||
|
data-poll-wrapper
|
||||||
|
>
|
||||||
|
<span data-poll="stream/{{ tab.key }}"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% if request.user.show_goal and not goal and tab.key == 'home' %}
|
{% if request.user.show_goal and not goal and tab.key == 'home' %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<div class="modal is-active" role="dialog" aria-modal="true" aria-labelledby="get_started_header">
|
<div class="modal is-active" role="dialog" aria-modal="true" aria-labelledby="get_started_header">
|
||||||
<div class="modal-background"></div>
|
<div class="modal-background"></div>
|
||||||
<div class="modal-card is-fullwidth">
|
<div class="modal-card is-fullwidth">
|
||||||
<header class="modal-card-head">
|
<header class="modal-card-head navbar">
|
||||||
<img
|
<img
|
||||||
class="image logo mr-2"
|
class="image logo mr-2"
|
||||||
src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}"
|
src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}"
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
<span class="icon icon-spinner is-pulled-left" aria-hidden="true"></span>
|
<span class="icon icon-spinner is-pulled-left" aria-hidden="true"></span>
|
||||||
<span>{% trans "In progress" %}</span>
|
<span>{% trans "In progress" %}</span>
|
||||||
<span class="is-pulled-right">
|
<span class="is-pulled-right">
|
||||||
<a href="#" class="button is-small">{% trans "Refresh" %}</a>
|
<a href="{% url 'import-status' job.id %}" class="button is-small">{% trans "Refresh" %}</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="is-flex">
|
<div class="is-flex">
|
||||||
|
@ -230,7 +230,7 @@
|
||||||
|
|
||||||
{% if not legacy %}
|
{% if not legacy %}
|
||||||
<div>
|
<div>
|
||||||
{% include 'snippets/pagination.html' with page=items %}
|
{% include 'snippets/pagination.html' with page=items path=page_path %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endspaceless %}{% endblock %}
|
{% endspaceless %}{% endblock %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends 'landing/layout.html' %}
|
{% extends 'landing/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load cache %}
|
{% load cache %}
|
||||||
{% load bookwyrm_tags %}
|
{% load landing_page_tags %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load book_display_tags %}
|
||||||
|
{% load rating_tags %}
|
||||||
{% load markdown %}
|
{% load markdown %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load rating_tags %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% if book %}
|
{% if book %}
|
||||||
|
|
45
bookwyrm/templates/lists/add_item_modal.html
Normal file
45
bookwyrm/templates/lists/add_item_modal.html
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
{% extends 'components/modal.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
{% load group_tags %}
|
||||||
|
|
||||||
|
{% block modal-title %}
|
||||||
|
{% if list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %}
|
||||||
|
{% blocktrans trimmed with title=book|book_title %}
|
||||||
|
Add "<em>{{ title }}</em>" to this list
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% blocktrans trimmed with title=book|book_title %}
|
||||||
|
Suggest "<em>{{ title }}</em>" for this list
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-form-open %}
|
||||||
|
<form
|
||||||
|
name="add-book-{{ book.id }}"
|
||||||
|
method="POST"
|
||||||
|
action="{% url 'list-add-book' %}{% if query %}?q={{ query }}{% endif %}"
|
||||||
|
>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-body %}
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="book" value="{{ book.id }}">
|
||||||
|
<input type="hidden" name="book_list" value="{{ list.id }}">
|
||||||
|
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||||
|
{% include "lists/item_notes_field.html" with form_id=id show_label=True %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-footer %}
|
||||||
|
<button type="submit" class="button is-link">
|
||||||
|
{% if list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %}
|
||||||
|
{% trans "Add" %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Suggest" %}
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-form-close %}</form>{% endblock %}
|
|
@ -1,5 +1,6 @@
|
||||||
{% extends 'lists/layout.html' %}
|
{% extends 'lists/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||||
|
@ -38,6 +39,17 @@
|
||||||
|
|
||||||
<div class="column ml-3">
|
<div class="column ml-3">
|
||||||
{% include 'snippets/book_titleby.html' %}
|
{% include 'snippets/book_titleby.html' %}
|
||||||
|
{% if item.notes %}
|
||||||
|
<div>
|
||||||
|
{% url 'user-feed' item.user|username as user_path %}
|
||||||
|
{% blocktrans trimmed with username=item.user.display_name %}
|
||||||
|
<a href="{{ user_path }}">{{ username }}</a> says:
|
||||||
|
{% endblocktrans %}
|
||||||
|
<p class="notification">
|
||||||
|
{{ item.notes }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{% trans "Suggested by" %}
|
{% trans "Suggested by" %}
|
||||||
|
|
20
bookwyrm/templates/lists/edit_item_form.html
Normal file
20
bookwyrm/templates/lists/edit_item_form.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<form
|
||||||
|
name="edit-notes-{{ item.id }}"
|
||||||
|
method="POST"
|
||||||
|
action="{% url 'list-item' list.id item.id %}"
|
||||||
|
>
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="book" value="{{ book.id }}">
|
||||||
|
<input type="hidden" name="book_list" value="{{ list.id }}">
|
||||||
|
<input type="hidden" name="user" value="{{ request.user.id }}">
|
||||||
|
{% include "lists/item_notes_field.html" with form_id=item.id %}
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<button type="submit" class="button is-success">
|
||||||
|
{% trans "Save" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
21
bookwyrm/templates/lists/item_notes_field.html
Normal file
21
bookwyrm/templates/lists/item_notes_field.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<div class="field">
|
||||||
|
<label
|
||||||
|
for="id_notes_{{ form_id }}"
|
||||||
|
class="{% if show_label %}label{% else %}is-sr-only{% endif %}"
|
||||||
|
>
|
||||||
|
{% trans "Notes:" %}
|
||||||
|
</label>
|
||||||
|
<div class="control">
|
||||||
|
<textarea
|
||||||
|
class="textarea"
|
||||||
|
id="id_notes_{{ form_id }}"
|
||||||
|
maxlength="300"
|
||||||
|
name="notes"
|
||||||
|
aria-describedby="notes_description_{{ form_id }}"
|
||||||
|
>{{ item.notes|default:'' }}</textarea>
|
||||||
|
</div>
|
||||||
|
<p class="help" id="notes_description_{{ form_id }}">
|
||||||
|
{% trans "An optional note that will be displayed with the book." %}
|
||||||
|
</p>
|
||||||
|
</div>
|
|
@ -1,8 +1,10 @@
|
||||||
{% extends 'lists/layout.html' %}
|
{% extends 'lists/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load rating_tags %}
|
||||||
{% load bookwyrm_group_tags %}
|
{% load group_tags %}
|
||||||
|
{% load book_display_tags %}
|
||||||
{% load markdown %}
|
{% load markdown %}
|
||||||
|
{% load utilities %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||||
|
@ -20,7 +22,7 @@
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
{% if request.user == list.user and pending_count %}
|
{% if request.user == list.user and pending_count %}
|
||||||
<div class="block content">
|
<div class="block content">
|
||||||
<p>
|
<p class="notification">
|
||||||
<a href="{% url 'list-curate' list.id %}">{{ pending_count }} book{{ pending_count|pluralize }} awaiting your approval</a>
|
<a href="{% url 'list-curate' list.id %}">{{ pending_count }} book{{ pending_count|pluralize }} awaiting your approval</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,21 +47,16 @@
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<li class="block mb-5">
|
<li class="block mb-5">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
{% with book=item.book %}
|
<div class="card-content">
|
||||||
<div
|
{% with book=item.book %}
|
||||||
class="
|
<div class="columns is-mobile">
|
||||||
card-content p-0 mb-0
|
<div class="column is-narrow is-cover">
|
||||||
columns is-gapless
|
|
||||||
is-mobile
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div class="column is-3-mobile is-2-tablet is-cover align to-t">
|
|
||||||
<a href="{{ item.book.local_path }}" aria-hidden="true">
|
<a href="{{ item.book.local_path }}" aria-hidden="true">
|
||||||
{% include 'snippets/book_cover.html' with cover_class='is-w-auto is-h-m-tablet is-align-items-flex-start' size='medium' %}
|
{% include 'snippets/book_cover.html' with cover_class='is-w-auto is-h-m-tablet is-align-items-flex-start' size='medium' %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column mx-3 my-2">
|
<div class="column">
|
||||||
<p>
|
<p>
|
||||||
{% include 'snippets/book_titleby.html' %}
|
{% include 'snippets/book_titleby.html' %}
|
||||||
</p>
|
</p>
|
||||||
|
@ -72,8 +69,52 @@
|
||||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
|
{% if item.notes %}
|
||||||
|
<div class="media notification">
|
||||||
|
<figure class="media-left" aria-hidden="true">
|
||||||
|
{% include "snippets/avatar.html" with user=item.user %}
|
||||||
|
</figure>
|
||||||
|
<div class="media-content">
|
||||||
|
<div class="content">
|
||||||
|
<header>
|
||||||
|
{% url 'user-feed' item.user|username as user_path %}
|
||||||
|
{% blocktrans trimmed with username=user.display_name %}
|
||||||
|
<a href="{{ user_path }}">{{ username }}</a> says:
|
||||||
|
{% endblocktrans %}
|
||||||
|
</header>
|
||||||
|
{{ item.notes|to_markdown|safe }}
|
||||||
|
</div>
|
||||||
|
{% if item.user == request.user %}
|
||||||
|
<div>
|
||||||
|
<details class="details-panel box">
|
||||||
|
<summary>
|
||||||
|
<span role="heading" aria-level="3">
|
||||||
|
{% trans "Edit notes" %}
|
||||||
|
<span class="details-close icon icon-pencil" aria-hidden></span>
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
{% include "lists/edit_item_form.html" with book=item.book %}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% elif item.user == request.user %}
|
||||||
|
<div>
|
||||||
|
<details class="details-panel box">
|
||||||
|
<summary>
|
||||||
|
<span role="heading" aria-level="3">
|
||||||
|
{% trans "Add notes" %}
|
||||||
|
<span class="details-close icon icon-plus" aria-hidden></span>
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
{% include "lists/edit_item_form.html" with book=item.book %}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<div class="card-footer is-stacked-mobile has-background-white-bis is-align-items-stretch">
|
<div class="card-footer is-stacked-mobile has-background-white-bis is-align-items-stretch">
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
<p>
|
<p>
|
||||||
|
@ -194,23 +235,19 @@
|
||||||
<div class="column ml-3">
|
<div class="column ml-3">
|
||||||
<p>{% include 'snippets/book_titleby.html' with book=book %}</p>
|
<p>{% include 'snippets/book_titleby.html' with book=book %}</p>
|
||||||
|
|
||||||
<form
|
{% join "add_item" list.id book.id as modal_id %}
|
||||||
class="mt-1"
|
<button
|
||||||
name="add-book-{{ book.id }}"
|
type="button"
|
||||||
method="post"
|
class="button is-small is-link"
|
||||||
action="{% url 'list-add-book' %}{% if query %}?q={{ query }}{% endif %}"
|
data-modal-open="{{ modal_id }}"
|
||||||
>
|
>
|
||||||
{% csrf_token %}
|
{% if list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %}
|
||||||
<input type="hidden" name="book" value="{{ book.id }}">
|
{% trans "Add" %}
|
||||||
<input type="hidden" name="list" value="{{ list.id }}">
|
{% else %}
|
||||||
<button type="submit" class="button is-small is-link">
|
{% trans "Suggest" %}
|
||||||
{% if list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %}
|
{% endif %}
|
||||||
{% trans "Add" %}
|
</button>
|
||||||
{% else %}
|
{% include "lists/add_item_modal.html" with id=modal_id %}
|
||||||
{% trans "Suggest" %}
|
|
||||||
{% endif %}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -224,7 +261,7 @@
|
||||||
<textarea
|
<textarea
|
||||||
readonly
|
readonly
|
||||||
class="textarea is-small"
|
class="textarea is-small"
|
||||||
aria-labelledby="embed-label"
|
aria-describedby="embed-label"
|
||||||
data-copytext
|
data-copytext
|
||||||
data-copytext-label="{% trans 'Copy embed code' %}"
|
data-copytext-label="{% trans 'Copy embed code' %}"
|
||||||
data-copytext-success="{% trans 'Copied!' %}"
|
data-copytext-success="{% trans 'Copied!' %}"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load notification_page_tags %}
|
||||||
{% related_status notification as related_status %}
|
{% related_status notification as related_status %}
|
||||||
<div class="notification {% if notification.id in unread %}has-background-primary{% endif %}">
|
<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="columns is-mobile {% if notification.id in unread %}has-text-white{% else %}has-text-grey{% endif %}">
|
||||||
|
|
|
@ -70,9 +70,7 @@
|
||||||
|
|
||||||
{% block modal-footer %}
|
{% block modal-footer %}
|
||||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||||
{% if not static %}
|
|
||||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal-form-close %}
|
{% block modal-form-close %}
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include "snippets/report_modal.html" with user=user active=True static=True %}
|
{% include "snippets/report_modal.html" with user=user active=True static=True id="report-modal" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,5 +1,78 @@
|
||||||
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
||||||
|
|
||||||
|
##### AhrefsBot #####
|
||||||
|
# see http://ahrefs.com/robot/
|
||||||
|
User-agent: AhrefsBot
|
||||||
|
Crawl-Delay: 10
|
||||||
|
#Disallow: /
|
||||||
|
|
||||||
|
|
||||||
|
##### SemrushBot #####
|
||||||
|
# see http://www.semrush.com/bot.html
|
||||||
|
User-agent: SemrushBot
|
||||||
|
Crawl-Delay: 10
|
||||||
|
#Disallow: /
|
||||||
|
|
||||||
|
# To block SemrushBot from crawling your site for different SEO and technical issues:
|
||||||
|
User-agent: SiteAuditBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
#To block SemrushBot from crawling your site for Backlink Audit tool:
|
||||||
|
User-agent: SemrushBot-BA
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
#To block SemrushBot from crawling your site for On Page SEO Checker tool and similar tools:
|
||||||
|
User-agent: SemrushBot-SI
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
#To block SemrushBot from checking URLs on your site for SWA tool:
|
||||||
|
User-agent: SemrushBot-SWA
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
#To block SemrushBot from crawling your site for Content Analyzer and Post Tracking tools:
|
||||||
|
User-agent: SemrushBot-CT
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
#To block SemrushBot from crawling your site for Brand Monitoring:
|
||||||
|
User-agent: SemrushBot-BM
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
#To block SplitSignalBot from crawling your site for SplitSignal tool:
|
||||||
|
User-agent: SplitSignalBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
#To block SemrushBot-COUB from crawling your site for Content Outline Builder tool:
|
||||||
|
User-agent: SemrushBot-COUB
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
|
||||||
|
##### DotBot #####
|
||||||
|
# see https://opensiteexplorer.org/dotbot
|
||||||
|
User-agent: dotbot
|
||||||
|
Crawl-delay: 10
|
||||||
|
#Disallow: /
|
||||||
|
|
||||||
|
|
||||||
|
##### BLEXBot #####
|
||||||
|
# see http://webmeup-crawler.com/
|
||||||
|
User-agent: BLEXBot
|
||||||
|
Crawl-delay: 10
|
||||||
|
#Disallow: /
|
||||||
|
|
||||||
|
|
||||||
|
##### MJ12bot #####
|
||||||
|
# see http://mj12bot.com/
|
||||||
|
User-Agent: MJ12bot
|
||||||
|
Crawl-Delay: 20
|
||||||
|
#Disallow: /
|
||||||
|
|
||||||
|
|
||||||
|
##### PetalBot #####
|
||||||
|
# see https://webmaster.petalsearch.com/site/petalbot
|
||||||
|
User-agent: PetalBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow: /static/js/
|
Disallow: /static/js/
|
||||||
Disallow: /static/css/
|
Disallow: /static/css/
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<header class="column">
|
<header class="column">
|
||||||
<h2 class="title is-5">
|
<h2 class="title is-5">
|
||||||
{{ domain.name }}
|
{{ domain.name }}
|
||||||
(<a href="http://{{ domain.domain }}" target="_blank" rel="noopener">{{ domain.domain }}</a>)
|
(<a href="http://{{ domain.domain }}" target="_blank" rel="noopener noreferrer">{{ domain.domain }}</a>)
|
||||||
</h2>
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
{% for link in links %}
|
{% for link in links %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="overflow-wrap-anywhere">
|
<td class="overflow-wrap-anywhere">
|
||||||
<a href="{{ link.url }}" target="_blank" rel="noopener">{{ link.url }}</a>
|
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer">{{ link.url }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'settings-user' link.added_by.id %}">@{{ link.added_by|username }}</a>
|
<a href="{% url 'settings-user' link.added_by.id %}">@{{ link.added_by|username }}</a>
|
||||||
|
|
|
@ -17,6 +17,19 @@
|
||||||
{% include 'settings/reports/report_preview.html' with report=report %}
|
{% include 'settings/reports/report_preview.html' with report=report %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<details class="details-panel box">
|
||||||
|
<summary>
|
||||||
|
<span class="title is-4">{% trans "Message reporter" %}</span>
|
||||||
|
<span class="details-close icon icon-x" aria-hidden></span>
|
||||||
|
</summary>
|
||||||
|
<div class="box">
|
||||||
|
{% trans "Update on your report:" as dm_template %}
|
||||||
|
{% include 'snippets/create_status/status.html' with type="direct" uuid=1 mention=report.reporter prepared_content=dm_template no_script=True %}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if report.statuses.exists %}
|
{% if report.statuses.exists %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h3 class="title is-4">{% trans "Reported statuses" %}</h3>
|
<h3 class="title is-4">{% trans "Reported statuses" %}</h3>
|
||||||
|
@ -68,9 +81,13 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<form class="block" name="report-comment" method="post" action="{% url 'settings-report' report.id %}">
|
<form class="block" name="report-comment" method="post" action="{% url 'settings-report' report.id %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<label for="report_comment" class="label">Comment on report</label>
|
<div class="field">
|
||||||
<textarea name="note" id="report_comment" class="textarea"></textarea>
|
<label for="report_comment" class="label">Comment on report</label>
|
||||||
<button class="button">{% trans "Comment" %}</button>
|
<textarea name="note" id="report_comment" class="textarea"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<button class="button">{% trans "Comment" %}</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
@ -45,7 +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 %}
|
||||||
>
|
>
|
||||||
{% include 'user/books_header.html' with shelf=shelf_tab %}
|
{% include "snippets/translated_shelf_name.html" with shelf=shelf_tab %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -92,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 %}
|
||||||
|
@ -104,7 +104,7 @@
|
||||||
(showing {{ start }}-{{ end }})
|
(showing {{ start }}-{{ end }})
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
<img class="avatar image {% if large %}is-96x96{% elif medium %}is-48x48{% else %}is-32x32{% endif %}" src="{% if user.avatar %}{% get_media_prefix %}{{ user.avatar }}{% else %}{% static "images/default_avi.jpg" %}{% endif %}" {% if ariaHide %}aria-hidden="true"{% endif %} alt="{{ user.alt_text }}">
|
<img
|
||||||
|
class="avatar image {% if large %}is-96x96{% elif medium %}is-48x48{% else %}is-32x32{% endif %}"
|
||||||
|
src="{% if user.avatar %}{% get_media_prefix %}{{ user.avatar }}{% else %}{% static "images/default_avi.jpg" %}{% endif %}"
|
||||||
|
{% if ariaHide %}aria-hidden="true"{% endif %}
|
||||||
|
alt="{{ user.alt_text }}"
|
||||||
|
>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -8,13 +8,14 @@ reply_parent: if applicable, the Status object that this post is in reply to
|
||||||
mention: a user who is @ mentioned by default in the post
|
mention: a user who is @ mentioned by default in the post
|
||||||
draft: an existing Status object that is providing default values for input fields
|
draft: an existing Status object that is providing default values for input fields
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
<textarea
|
<div class="control">
|
||||||
name="content"
|
<textarea
|
||||||
class="textarea save-draft"
|
name="content"
|
||||||
{% if not draft %}data-cache-draft="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}"{% endif %}
|
class="textarea save-draft"
|
||||||
id="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}{{ uuid }}"
|
{% if not draft %}data-cache-draft="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}"{% endif %}
|
||||||
placeholder="{{ placeholder }}"
|
id="id_content_{{ type }}_{{ book.id }}{{ reply_parent.id }}{{ uuid }}"
|
||||||
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}"
|
placeholder="{{ placeholder }}"
|
||||||
{% if not optional and type != "quotation" and type != "review" %}required{% endif %}
|
aria-label="{% if reply_parent %}{% trans 'Reply' %}{% else %}{% trans 'Content' %}{% endif %}"
|
||||||
>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mention %}@{{ mention|username }} {% endif %}{% firstof draft.raw_content draft.content '' %}</textarea>
|
{% if not optional and type != "quotation" and type != "review" %}required{% endif %}
|
||||||
|
>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mention %}@{{ mention|username }} {% endif %}{{ prepared_content }}{% firstof draft.raw_content draft.content '' %}</textarea>
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load utilities %}
|
{% load utilities %}
|
||||||
{% load status_display %}
|
{% load status_display %}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow control">
|
||||||
<button class="button is-link" type="submit">
|
<button class="button is-link" type="submit">
|
||||||
<span class="icon icon-spinner" aria-hidden="true"></span>
|
<span class="icon icon-spinner" aria-hidden="true"></span>
|
||||||
<span>{% trans "Post" %}</span>
|
<span>{% trans "Post" %}</span>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -38,9 +38,11 @@
|
||||||
{% block filter_fields %}
|
{% block filter_fields %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="button is-primary is-small">
|
<div class="control">
|
||||||
|
<button type="submit" class="button is-primary">
|
||||||
{% trans "Apply filters" %}
|
{% trans "Apply filters" %}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
|
@ -2,15 +2,15 @@
|
||||||
{% if rating %}
|
{% if rating %}
|
||||||
|
|
||||||
{% 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 %}
|
{% 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 "<a href='{{ book_path }}'>{{ book_title }}</a>" ({{ display_rating }} star): {{ review_title }}
|
Review of "{{ book_title }}" ({{ display_rating }} star): {{ review_title }}
|
||||||
{% plural %}
|
{% plural %}
|
||||||
Review of "<a href='{{ book_path }}'>{{ book_title }}</a>" ({{ display_rating }} stars): {{ review_title }}
|
Review of "{{ book_title }}" ({{ display_rating }} stars): {{ review_title }}
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
{% blocktrans trimmed with book_title=book.title|safe book_path=book.local_path review_title=name|safe %}
|
{% blocktrans trimmed with book_title=book.title|safe book_path=book.local_path review_title=name|safe %}
|
||||||
Review of "<a href='{{ book_path }}'>{{ book_title }}</a>": {{ review_title }}
|
Review of "{{ book_title }}": {{ review_title }}
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="reporter" value="{{ request.user.id }}">
|
<input type="hidden" name="reporter" value="{{ request.user.id }}">
|
||||||
<input type="hidden" name="user" value="{{ user.id }}">
|
<input type="hidden" name="user" value="{{ user.id }}">
|
||||||
{% if status %}
|
{% if status_id %}
|
||||||
<input type="hidden" name="statuses" value="{{ status_id }}">
|
<input type="hidden" name="statuses" value="{{ status_id }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if link %}
|
{% if link %}
|
||||||
|
@ -50,9 +50,7 @@
|
||||||
{% block modal-footer %}
|
{% block modal-footer %}
|
||||||
|
|
||||||
<button class="button is-success" type="submit">{% trans "Submit" %}</button>
|
<button class="button is-success" type="submit">{% trans "Submit" %}</button>
|
||||||
{% if not static %}
|
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends 'components/dropdown.html' %}
|
{% extends 'components/dropdown.html' %}
|
||||||
{% load i18n %}
|
{% load shelf_tags %}
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
{% load utilities %}
|
{% load utilities %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block dropdown-trigger %}
|
{% block dropdown-trigger %}
|
||||||
<span>{% trans "Move book" %}</span>
|
<span>{% trans "Move book" %}</span>
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
{% if shelf.editable %}
|
{% if shelf.editable %}
|
||||||
<li role="menuitem" class="dropdown-item p-0">
|
<li role="menuitem" class="dropdown-item p-0">
|
||||||
<form name="shelve" action="/shelve/" method="post">
|
<form name="editable-shelve-{{ uuid }}" action="/shelve/" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ book.id }}">
|
<input type="hidden" name="book" value="{{ book.id }}">
|
||||||
<input type="hidden" name="change-shelf-from" value="{{ current.identifier }}">
|
<input type="hidden" name="change-shelf-from" value="{{ current.identifier }}">
|
||||||
|
@ -67,7 +67,7 @@
|
||||||
{% if user_shelf in book.shelves.all %}
|
{% if user_shelf in book.shelves.all %}
|
||||||
<li class="navbar-divider m-0" role="separator" ></li>
|
<li class="navbar-divider m-0" role="separator" ></li>
|
||||||
<li role="menuitem" class="dropdown-item p-0">
|
<li role="menuitem" class="dropdown-item p-0">
|
||||||
<form name="shelve" action="/unshelve/" method="post">
|
<form name="shelve-{{ user_shelf.identifier }}-{{ book.id }}-{{ uuid }}" action="/unshelve/" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ book.id }}">
|
<input type="hidden" name="book" value="{{ book.id }}">
|
||||||
<input type="hidden" name="shelf" value="{{ user_shelf.id }}">
|
<input type="hidden" name="shelf" value="{{ user_shelf.id }}">
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="navbar-divider" role="separator" ></li>
|
<li class="navbar-divider" role="separator" ></li>
|
||||||
<li role="menuitem" class="dropdown-item p-0">
|
<li role="menuitem" class="dropdown-item p-0">
|
||||||
<form name="shelve" action="/unshelve/" method="post">
|
<form name="un-shelve-{{ shelf.identifier }}-{{ book.id }}-{{ uuid }}" action="/unshelve/" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ book.id }}">
|
<input type="hidden" name="book" value="{{ book.id }}">
|
||||||
<input type="hidden" name="shelf" value="{{ shelf.id }}">
|
<input type="hidden" name="shelf" value="{{ shelf.id }}">
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
{% load utilities %}
|
{% load utilities %}
|
||||||
|
{% load shelf_tags %}
|
||||||
|
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
|
|
||||||
{% with book.id|uuid as uuid %}
|
{% with book.id|uuid as uuid %}
|
||||||
{% active_shelf book as active_shelf %}
|
{% active_shelf book as active_shelf %}
|
||||||
{% latest_read_through book request.user as readthrough %}
|
{% latest_read_through book request.user as readthrough %}
|
||||||
|
{% with active_shelf_book=active_shelf.book %}
|
||||||
<div class="field has-addons mb-0 has-text-weight-normal" data-shelve-button-book="{{ book.id }}">
|
<div class="field has-addons mb-0 has-text-weight-normal" data-shelve-button-book="{{ book.id }}">
|
||||||
{% if switch_mode and active_shelf.book != book %}
|
{% if switch_mode and active_shelf_book != book %}
|
||||||
<div class="control">
|
<div class="control">
|
||||||
{% include 'snippets/switch_edition_button.html' with edition=book size='is-small' %}
|
{% include 'snippets/switch_edition_button.html' with edition=book size='is-small' %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,16 +21,17 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% join "want_to_read" uuid as modal_id %}
|
{% join "want_to_read" uuid as modal_id %}
|
||||||
{% include 'snippets/reading_modals/want_to_read_modal.html' with book=active_shelf.book id=modal_id class="" %}
|
{% include 'snippets/reading_modals/want_to_read_modal.html' with book=active_shelf_book id=modal_id class="" %}
|
||||||
|
|
||||||
{% join "start_reading" uuid as modal_id %}
|
{% join "start_reading" uuid as modal_id %}
|
||||||
{% include 'snippets/reading_modals/start_reading_modal.html' with book=active_shelf.book id=modal_id class="" %}
|
{% include 'snippets/reading_modals/start_reading_modal.html' with book=active_shelf_book id=modal_id class="" %}
|
||||||
|
|
||||||
{% join "finish_reading" uuid as modal_id %}
|
{% join "finish_reading" uuid as modal_id %}
|
||||||
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book id=modal_id readthrough=readthrough class="" %}
|
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
|
||||||
|
|
||||||
{% join "progress_update" uuid as modal_id %}
|
{% join "progress_update" uuid as modal_id %}
|
||||||
{% include 'snippets/reading_modals/progress_update_modal.html' with book=active_shelf.book id=modal_id readthrough=readthrough class="" %}
|
{% include 'snippets/reading_modals/progress_update_modal.html' with book=active_shelf_book id=modal_id readthrough=readthrough class="" %}
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
{% load utilities %}
|
{% load utilities %}
|
||||||
|
{% load shelf_tags %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% with next_shelf_identifier=active_shelf.shelf.identifier|next_shelf %}
|
{% with next_shelf_identifier=active_shelf.shelf.identifier|next_shelf %}
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
<form name="shelve-{{ uuid }}-{{ shelf.identifier }}" action="/shelve/" method="post" autocomplete="off">
|
<form name="shelve-{{ uuid }}-{{ shelf.identifier }}" action="/shelve/" method="post" autocomplete="off">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
|
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
|
||||||
<button class="button {{ class }}" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>
|
<button class="button {{ class }}" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if book|is_book_on_shelf:shelf %} disabled {% endif %}>
|
||||||
<span>{{ shelf.name }}</span>
|
<span>{{ shelf.name }}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
|
|
||||||
{% if active_shelf.shelf %}
|
{% if active_shelf.shelf %}
|
||||||
<li role="menuitem" class="dropdown-item p-0" data-extra-options>
|
<li role="menuitem" class="dropdown-item p-0" data-extra-options>
|
||||||
<form name="shelve" action="/unshelve/" method="post">
|
<form name="unshelve-{{ uuid }}-{{ shelf.identifier }}" action="/unshelve/" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
|
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
|
||||||
<input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}">
|
<input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% load bookwyrm_tags %}
|
|
||||||
{% load utilities %}
|
{% load utilities %}
|
||||||
|
{% load shelf_tags %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% with next_shelf_identifier=active_shelf.shelf.identifier|next_shelf %}
|
{% with next_shelf_identifier=active_shelf.shelf.identifier|next_shelf %}
|
||||||
|
@ -45,7 +45,13 @@
|
||||||
<form name="shelve-{{ uuid }}-{{ shelf.identifier }}" action="/shelve/" method="post">
|
<form name="shelve-{{ uuid }}-{{ shelf.identifier }}" action="/shelve/" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
|
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
|
||||||
<button class="button {{ class }}" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>
|
<button
|
||||||
|
class="button {{ class }}"
|
||||||
|
name="shelf"
|
||||||
|
type="submit"
|
||||||
|
value="{{ shelf.identifier }}"
|
||||||
|
{% if book|is_book_on_shelf:shelf %} disabled {% endif %}
|
||||||
|
>
|
||||||
<span>{{ shelf.name }}</span>
|
<span>{{ shelf.name }}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load book_display_tags %}
|
||||||
|
{% load rating_tags %}
|
||||||
{% load markdown %}
|
{% load markdown %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
|
|
||||||
{% load bookwyrm_tags %}
|
{% load book_display_tags %}
|
||||||
{% load markdown %}
|
{% load markdown %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load cache %}
|
|
||||||
|
|
||||||
{% if not hide_book %}
|
{% if not hide_book %}
|
||||||
{% cache 259200 generated_status_book status.id %}
|
|
||||||
{% with book=status.book|default:status.mention_books.first %}
|
{% with book=status.book|default:status.mention_books.first %}
|
||||||
<div class="columns is-mobile is-gapless">
|
<div class="columns is-mobile is-gapless">
|
||||||
<a class="column is-cover is-narrow" href="{{ book.local_path }}">
|
<a class="column is-cover is-narrow" href="{{ book.local_path }}">
|
||||||
|
@ -26,7 +24,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endcache %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endspaceless %}
|
{% endspaceless %}
|
||||||
|
|
|
@ -10,7 +10,9 @@
|
||||||
|
|
||||||
{% block dropdown-list %}
|
{% block dropdown-list %}
|
||||||
<li role="menuitem">
|
<li role="menuitem">
|
||||||
<a href="{% url 'direct-messages-user' user|username %}" class="button is-fullwidth is-small">{% trans "Send direct message" %}</a>
|
<div class="control">
|
||||||
|
<a href="{% url 'direct-messages-user' user|username %}" class="button is-fullwidth is-small">{% trans "Send direct message" %}</a>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li role="menuitem">
|
<li role="menuitem">
|
||||||
{% include 'snippets/report_button.html' with user=user class="is-fullwidth" %}
|
{% include 'snippets/report_button.html' with user=user class="is-fullwidth" %}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
{% load utilities %}
|
{% load utilities %}
|
||||||
{% load markdown %}
|
{% load markdown %}
|
||||||
{% load layout %}
|
{% load layout %}
|
||||||
{% load bookwyrm_group_tags %}
|
{% load group_tags %}
|
||||||
|
|
||||||
{% block title %}{{ user.display_name }}{% endblock %}
|
{% block title %}{{ user.display_name }}{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
{% load utilities %}
|
{% load utilities %}
|
||||||
{% load bookwyrm_tags %}
|
{% load user_page_tags %}
|
||||||
|
|
||||||
<div class="media block">
|
<div class="media block">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
|
|
17
bookwyrm/templatetags/book_display_tags.py
Normal file
17
bookwyrm/templatetags/book_display_tags.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
""" template filters """
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="book_description")
|
||||||
|
def get_book_description(book):
|
||||||
|
"""use the work's text if the book doesn't have it"""
|
||||||
|
return book.description or book.parent_work.description
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=False)
|
||||||
|
def get_book_file_links(book):
|
||||||
|
"""links for a book"""
|
||||||
|
return book.file_links.filter(domain__status="approved")
|
|
@ -1,206 +0,0 @@
|
||||||
""" template filters """
|
|
||||||
from django import template
|
|
||||||
from django.db.models import Avg, StdDev, Count, F, Q
|
|
||||||
|
|
||||||
from bookwyrm import models
|
|
||||||
from bookwyrm.utils import cache
|
|
||||||
from bookwyrm.views.feed import get_suggested_books
|
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="rating")
|
|
||||||
def get_rating(book, user):
|
|
||||||
"""get the overall rating of a book"""
|
|
||||||
queryset = models.Review.privacy_filter(user).filter(
|
|
||||||
book__parent_work__editions=book
|
|
||||||
)
|
|
||||||
return queryset.aggregate(Avg("rating"))["rating__avg"]
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="user_rating")
|
|
||||||
def get_user_rating(book, user):
|
|
||||||
"""get a user's rating of a book"""
|
|
||||||
rating = (
|
|
||||||
models.Review.objects.filter(
|
|
||||||
user=user,
|
|
||||||
book=book,
|
|
||||||
rating__isnull=False,
|
|
||||||
deleted=False,
|
|
||||||
)
|
|
||||||
.order_by("-published_date")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if rating:
|
|
||||||
return rating.rating
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="book_description")
|
|
||||||
def get_book_description(book):
|
|
||||||
"""use the work's text if the book doesn't have it"""
|
|
||||||
return book.description or book.parent_work.description
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="next_shelf")
|
|
||||||
def get_next_shelf(current_shelf):
|
|
||||||
"""shelf you'd use to update reading progress"""
|
|
||||||
if current_shelf == "to-read":
|
|
||||||
return "reading"
|
|
||||||
if current_shelf == "reading":
|
|
||||||
return "read"
|
|
||||||
if current_shelf == "read":
|
|
||||||
return "complete"
|
|
||||||
return "to-read"
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="load_subclass")
|
|
||||||
def load_subclass(status):
|
|
||||||
"""sometimes you didn't select_subclass"""
|
|
||||||
if hasattr(status, "quotation"):
|
|
||||||
return status.quotation
|
|
||||||
if hasattr(status, "review"):
|
|
||||||
return status.review
|
|
||||||
if hasattr(status, "comment"):
|
|
||||||
return status.comment
|
|
||||||
if hasattr(status, "generatednote"):
|
|
||||||
return status.generatednote
|
|
||||||
return status
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=False)
|
|
||||||
def get_book_superlatives():
|
|
||||||
"""get book stats for the about page"""
|
|
||||||
total_ratings = models.Review.objects.filter(local=True, deleted=False).count()
|
|
||||||
data = {}
|
|
||||||
data["top_rated"] = (
|
|
||||||
models.Work.objects.annotate(
|
|
||||||
rating=Avg(
|
|
||||||
"editions__review__rating",
|
|
||||||
filter=Q(editions__review__local=True, editions__review__deleted=False),
|
|
||||||
),
|
|
||||||
rating_count=Count(
|
|
||||||
"editions__review",
|
|
||||||
filter=Q(editions__review__local=True, editions__review__deleted=False),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.annotate(weighted=F("rating") * F("rating_count") / total_ratings)
|
|
||||||
.filter(rating__gt=4, weighted__gt=0)
|
|
||||||
.order_by("-weighted")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
data["controversial"] = (
|
|
||||||
models.Work.objects.annotate(
|
|
||||||
deviation=StdDev(
|
|
||||||
"editions__review__rating",
|
|
||||||
filter=Q(editions__review__local=True, editions__review__deleted=False),
|
|
||||||
),
|
|
||||||
rating_count=Count(
|
|
||||||
"editions__review",
|
|
||||||
filter=Q(editions__review__local=True, editions__review__deleted=False),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.annotate(weighted=F("deviation") * F("rating_count") / total_ratings)
|
|
||||||
.filter(weighted__gt=0)
|
|
||||||
.order_by("-weighted")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
data["wanted"] = (
|
|
||||||
models.Work.objects.annotate(
|
|
||||||
shelf_count=Count(
|
|
||||||
"editions__shelves", filter=Q(editions__shelves__identifier="to-read")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.order_by("-shelf_count")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=False)
|
|
||||||
def related_status(notification):
|
|
||||||
"""for notifications"""
|
|
||||||
if not notification.related_status:
|
|
||||||
return None
|
|
||||||
return load_subclass(notification.related_status)
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
|
||||||
def active_shelf(context, book):
|
|
||||||
"""check what shelf a user has a book on, if any"""
|
|
||||||
user = context["request"].user
|
|
||||||
return (
|
|
||||||
cache.get_or_set(
|
|
||||||
f"active_shelf-{user.id}-{book.id}",
|
|
||||||
lambda u, b: (
|
|
||||||
models.ShelfBook.objects.filter(
|
|
||||||
shelf__user=u,
|
|
||||||
book__parent_work__editions=b,
|
|
||||||
).first()
|
|
||||||
),
|
|
||||||
user,
|
|
||||||
book,
|
|
||||||
timeout=15552000,
|
|
||||||
)
|
|
||||||
or {"book": book}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=False)
|
|
||||||
def latest_read_through(book, user):
|
|
||||||
"""the most recent read activity"""
|
|
||||||
return cache.get_or_set(
|
|
||||||
f"latest_read_through-{user.id}-{book.id}",
|
|
||||||
lambda u, b: (
|
|
||||||
models.ReadThrough.objects.filter(user=u, book=b, is_active=True)
|
|
||||||
.order_by("-start_date")
|
|
||||||
.first()
|
|
||||||
),
|
|
||||||
user,
|
|
||||||
book,
|
|
||||||
timeout=15552000,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=False)
|
|
||||||
def get_landing_books():
|
|
||||||
"""list of books for the landing page"""
|
|
||||||
return list(
|
|
||||||
set(
|
|
||||||
models.Edition.objects.filter(
|
|
||||||
review__published_date__isnull=False,
|
|
||||||
review__deleted=False,
|
|
||||||
review__user__local=True,
|
|
||||||
review__privacy__in=["public", "unlisted"],
|
|
||||||
)
|
|
||||||
.exclude(cover__exact="")
|
|
||||||
.distinct()
|
|
||||||
.order_by("-review__published_date")[:6]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
|
||||||
def mutuals_count(context, user):
|
|
||||||
"""how many users that you follow, follow them"""
|
|
||||||
viewer = context["request"].user
|
|
||||||
if not viewer.is_authenticated:
|
|
||||||
return None
|
|
||||||
return user.followers.filter(followers=viewer).count()
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
|
||||||
def suggested_books(context):
|
|
||||||
"""get books for suggested books panel"""
|
|
||||||
# this happens here instead of in the view so that the template snippet can
|
|
||||||
# be cached in the template
|
|
||||||
return get_suggested_books(context["request"].user)
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=False)
|
|
||||||
def get_book_file_links(book):
|
|
||||||
"""links for a book"""
|
|
||||||
return book.file_links.filter(domain__status="approved")
|
|
28
bookwyrm/templatetags/feed_page_tags.py
Normal file
28
bookwyrm/templatetags/feed_page_tags.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
""" tags used on the feed pages """
|
||||||
|
from django import template
|
||||||
|
from bookwyrm.views.feed import get_suggested_books
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="load_subclass")
|
||||||
|
def load_subclass(status):
|
||||||
|
"""sometimes you didn't select_subclass"""
|
||||||
|
if hasattr(status, "quotation"):
|
||||||
|
return status.quotation
|
||||||
|
if hasattr(status, "review"):
|
||||||
|
return status.review
|
||||||
|
if hasattr(status, "comment"):
|
||||||
|
return status.comment
|
||||||
|
if hasattr(status, "generatednote"):
|
||||||
|
return status.generatednote
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def suggested_books(context):
|
||||||
|
"""get books for suggested books panel"""
|
||||||
|
# this happens here instead of in the view so that the template snippet can
|
||||||
|
# be cached in the template
|
||||||
|
return get_suggested_books(context["request"].user)
|
76
bookwyrm/templatetags/landing_page_tags.py
Normal file
76
bookwyrm/templatetags/landing_page_tags.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
""" template filters """
|
||||||
|
from django import template
|
||||||
|
from django.db.models import Avg, StdDev, Count, F, Q
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=False)
|
||||||
|
def get_book_superlatives():
|
||||||
|
"""get book stats for the about page"""
|
||||||
|
total_ratings = models.Review.objects.filter(local=True, deleted=False).count()
|
||||||
|
data = {}
|
||||||
|
data["top_rated"] = (
|
||||||
|
models.Work.objects.annotate(
|
||||||
|
rating=Avg(
|
||||||
|
"editions__review__rating",
|
||||||
|
filter=Q(editions__review__local=True, editions__review__deleted=False),
|
||||||
|
),
|
||||||
|
rating_count=Count(
|
||||||
|
"editions__review",
|
||||||
|
filter=Q(editions__review__local=True, editions__review__deleted=False),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotate(weighted=F("rating") * F("rating_count") / total_ratings)
|
||||||
|
.filter(rating__gt=4, weighted__gt=0)
|
||||||
|
.order_by("-weighted")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
data["controversial"] = (
|
||||||
|
models.Work.objects.annotate(
|
||||||
|
deviation=StdDev(
|
||||||
|
"editions__review__rating",
|
||||||
|
filter=Q(editions__review__local=True, editions__review__deleted=False),
|
||||||
|
),
|
||||||
|
rating_count=Count(
|
||||||
|
"editions__review",
|
||||||
|
filter=Q(editions__review__local=True, editions__review__deleted=False),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotate(weighted=F("deviation") * F("rating_count") / total_ratings)
|
||||||
|
.filter(weighted__gt=0)
|
||||||
|
.order_by("-weighted")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
data["wanted"] = (
|
||||||
|
models.Work.objects.annotate(
|
||||||
|
shelf_count=Count(
|
||||||
|
"editions__shelves", filter=Q(editions__shelves__identifier="to-read")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("-shelf_count")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=False)
|
||||||
|
def get_landing_books():
|
||||||
|
"""list of books for the landing page"""
|
||||||
|
return list(
|
||||||
|
set(
|
||||||
|
models.Edition.objects.filter(
|
||||||
|
review__published_date__isnull=False,
|
||||||
|
review__deleted=False,
|
||||||
|
review__user__local=True,
|
||||||
|
review__privacy__in=["public", "unlisted"],
|
||||||
|
)
|
||||||
|
.exclude(cover__exact="")
|
||||||
|
.distinct()
|
||||||
|
.order_by("-review__published_date")[:6]
|
||||||
|
)
|
||||||
|
)
|
14
bookwyrm/templatetags/notification_page_tags.py
Normal file
14
bookwyrm/templatetags/notification_page_tags.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
""" tags used on the feed pages """
|
||||||
|
from django import template
|
||||||
|
from bookwyrm.templatetags.feed_page_tags import load_subclass
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=False)
|
||||||
|
def related_status(notification):
|
||||||
|
"""for notifications"""
|
||||||
|
if not notification.related_status:
|
||||||
|
return None
|
||||||
|
return load_subclass(notification.related_status)
|
42
bookwyrm/templatetags/rating_tags.py
Normal file
42
bookwyrm/templatetags/rating_tags.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
""" template filters """
|
||||||
|
from django import template
|
||||||
|
from django.db.models import Avg
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm.utils import cache
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="rating")
|
||||||
|
def get_rating(book, user):
|
||||||
|
"""get the overall rating of a book"""
|
||||||
|
return cache.get_or_set(
|
||||||
|
f"book-rating-{book.parent_work.id}-{user.id}",
|
||||||
|
lambda u, b: models.Review.privacy_filter(u)
|
||||||
|
.filter(book__parent_work__editions=b, rating__gt=0)
|
||||||
|
.aggregate(Avg("rating"))["rating__avg"]
|
||||||
|
or 0,
|
||||||
|
user,
|
||||||
|
book,
|
||||||
|
timeout=15552000,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="user_rating")
|
||||||
|
def get_user_rating(book, user):
|
||||||
|
"""get a user's rating of a book"""
|
||||||
|
rating = (
|
||||||
|
models.Review.objects.filter(
|
||||||
|
user=user,
|
||||||
|
book=book,
|
||||||
|
rating__isnull=False,
|
||||||
|
deleted=False,
|
||||||
|
)
|
||||||
|
.order_by("-published_date")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if rating:
|
||||||
|
return rating.rating
|
||||||
|
return 0
|
68
bookwyrm/templatetags/shelf_tags.py
Normal file
68
bookwyrm/templatetags/shelf_tags.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
""" Filters and tags related to shelving books """
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm.utils import cache
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="is_book_on_shelf")
|
||||||
|
def get_is_book_on_shelf(book, shelf):
|
||||||
|
"""is a book on a shelf"""
|
||||||
|
return cache.get_or_set(
|
||||||
|
f"book-on-shelf-{book.id}-{shelf.id}",
|
||||||
|
lambda b, s: s.books.filter(id=b.id).exists(),
|
||||||
|
book,
|
||||||
|
shelf,
|
||||||
|
timeout=15552000,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="next_shelf")
|
||||||
|
def get_next_shelf(current_shelf):
|
||||||
|
"""shelf you'd use to update reading progress"""
|
||||||
|
if current_shelf == "to-read":
|
||||||
|
return "reading"
|
||||||
|
if current_shelf == "reading":
|
||||||
|
return "read"
|
||||||
|
if current_shelf == "read":
|
||||||
|
return "complete"
|
||||||
|
return "to-read"
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def active_shelf(context, book):
|
||||||
|
"""check what shelf a user has a book on, if any"""
|
||||||
|
user = context["request"].user
|
||||||
|
return cache.get_or_set(
|
||||||
|
f"active_shelf-{user.id}-{book.id}",
|
||||||
|
lambda u, b: (
|
||||||
|
models.ShelfBook.objects.filter(
|
||||||
|
shelf__user=u,
|
||||||
|
book__parent_work__editions=b,
|
||||||
|
).first()
|
||||||
|
or False
|
||||||
|
),
|
||||||
|
user,
|
||||||
|
book,
|
||||||
|
timeout=15552000,
|
||||||
|
) or {"book": book}
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=False)
|
||||||
|
def latest_read_through(book, user):
|
||||||
|
"""the most recent read activity"""
|
||||||
|
return cache.get_or_set(
|
||||||
|
f"latest_read_through-{user.id}-{book.id}",
|
||||||
|
lambda u, b: (
|
||||||
|
models.ReadThrough.objects.filter(user=u, book=b, is_active=True)
|
||||||
|
.order_by("-start_date")
|
||||||
|
.first()
|
||||||
|
or False
|
||||||
|
),
|
||||||
|
user,
|
||||||
|
book,
|
||||||
|
timeout=15552000,
|
||||||
|
)
|
14
bookwyrm/templatetags/user_page_tags.py
Normal file
14
bookwyrm/templatetags/user_page_tags.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
""" template filters """
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def mutuals_count(context, user):
|
||||||
|
"""how many users that you follow, follow them"""
|
||||||
|
viewer = context["request"].user
|
||||||
|
if not viewer.is_authenticated:
|
||||||
|
return None
|
||||||
|
return user.followers.filter(followers=viewer).count()
|
|
@ -1,6 +1,9 @@
|
||||||
""" testing activitystreams """
|
""" testing activitystreams """
|
||||||
|
from datetime import datetime
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from bookwyrm import activitystreams, models
|
from bookwyrm import activitystreams, models
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,13 +54,63 @@ class Activitystreams(TestCase):
|
||||||
"""the abstract base class for stream objects"""
|
"""the abstract base class for stream objects"""
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.test_stream.stream_id(self.local_user),
|
self.test_stream.stream_id(self.local_user),
|
||||||
"{}-test".format(self.local_user.id),
|
f"{self.local_user.id}-test",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.test_stream.unread_id(self.local_user),
|
self.test_stream.unread_id(self.local_user),
|
||||||
"{}-test-unread".format(self.local_user.id),
|
f"{self.local_user.id}-test-unread",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_unread_by_status_type_id(self, *_):
|
||||||
|
"""stream for status type"""
|
||||||
|
self.assertEqual(
|
||||||
|
self.test_stream.unread_by_status_type_id(self.local_user),
|
||||||
|
f"{self.local_user.id}-test-unread-by-type",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_rank(self, *_):
|
||||||
|
"""sort order"""
|
||||||
|
date = datetime(2022, 1, 28, 0, 0, tzinfo=timezone.utc)
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
user=self.remote_user,
|
||||||
|
content="hi",
|
||||||
|
privacy="direct",
|
||||||
|
published_date=date,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
str(self.test_stream.get_rank(status)),
|
||||||
|
"1643328000.0",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_activity_stream(self, *_):
|
||||||
|
"""load statuses"""
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
user=self.remote_user,
|
||||||
|
content="hi",
|
||||||
|
privacy="direct",
|
||||||
|
)
|
||||||
|
status2 = models.Comment.objects.create(
|
||||||
|
user=self.remote_user,
|
||||||
|
content="hi",
|
||||||
|
privacy="direct",
|
||||||
|
book=self.book,
|
||||||
|
)
|
||||||
|
models.Comment.objects.create(
|
||||||
|
user=self.remote_user,
|
||||||
|
content="hi",
|
||||||
|
privacy="direct",
|
||||||
|
book=self.book,
|
||||||
|
)
|
||||||
|
with patch("bookwyrm.activitystreams.r.set"), patch(
|
||||||
|
"bookwyrm.activitystreams.r.delete"
|
||||||
|
), patch("bookwyrm.activitystreams.ActivityStream.get_store") as redis_mock:
|
||||||
|
redis_mock.return_value = [status.id, status2.id]
|
||||||
|
result = self.test_stream.get_activity_stream(self.local_user)
|
||||||
|
self.assertEqual(result.count(), 2)
|
||||||
|
self.assertEqual(result.first(), status2)
|
||||||
|
self.assertEqual(result.last(), status)
|
||||||
|
self.assertIsInstance(result.first(), models.Comment)
|
||||||
|
|
||||||
def test_abstractstream_get_audience(self, *_):
|
def test_abstractstream_get_audience(self, *_):
|
||||||
"""get a list of users that should see a status"""
|
"""get a list of users that should see a status"""
|
||||||
status = models.Status.objects.create(
|
status = models.Status.objects.create(
|
||||||
|
|
|
@ -52,3 +52,29 @@ class Activitystreams(TestCase):
|
||||||
# yes book, yes audience
|
# yes book, yes audience
|
||||||
result = activitystreams.BooksStream().get_statuses_for_user(self.local_user)
|
result = activitystreams.BooksStream().get_statuses_for_user(self.local_user)
|
||||||
self.assertEqual(list(result), [status])
|
self.assertEqual(list(result), [status])
|
||||||
|
|
||||||
|
def test_book_statuses(self, *_):
|
||||||
|
"""statuses about a book"""
|
||||||
|
alt_book = models.Edition.objects.create(
|
||||||
|
title="hi", parent_work=self.book.parent_work
|
||||||
|
)
|
||||||
|
status = models.Status.objects.create(
|
||||||
|
user=self.local_user, content="hi", privacy="public"
|
||||||
|
)
|
||||||
|
status = models.Comment.objects.create(
|
||||||
|
user=self.remote_user, content="hi", privacy="public", book=alt_book
|
||||||
|
)
|
||||||
|
models.ShelfBook.objects.create(
|
||||||
|
user=self.local_user,
|
||||||
|
shelf=self.local_user.shelf_set.first(),
|
||||||
|
book=self.book,
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"bookwyrm.activitystreams.BooksStream.bulk_add_objects_to_store"
|
||||||
|
) as redis_mock:
|
||||||
|
activitystreams.BooksStream().add_book_statuses(self.local_user, self.book)
|
||||||
|
args = redis_mock.call_args[0]
|
||||||
|
queryset = args[0]
|
||||||
|
self.assertEqual(queryset.count(), 1)
|
||||||
|
self.assertTrue(status in queryset)
|
||||||
|
self.assertEqual(args[1], f"{self.local_user.id}-books")
|
||||||
|
|
|
@ -4,8 +4,8 @@ from django.test import TestCase
|
||||||
import responses
|
import responses
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.connectors import abstract_connector
|
from bookwyrm.connectors import abstract_connector, ConnectorException
|
||||||
from bookwyrm.connectors.abstract_connector import Mapping
|
from bookwyrm.connectors.abstract_connector import Mapping, get_data
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
@ -163,3 +163,11 @@ class AbstractConnector(TestCase):
|
||||||
author.refresh_from_db()
|
author.refresh_from_db()
|
||||||
self.assertEqual(author.name, "Test")
|
self.assertEqual(author.name, "Test")
|
||||||
self.assertEqual(author.isni, "hi")
|
self.assertEqual(author.isni, "hi")
|
||||||
|
|
||||||
|
def test_get_data_invalid_url(self):
|
||||||
|
"""load json data from an arbitrary url"""
|
||||||
|
with self.assertRaises(ConnectorException):
|
||||||
|
get_data("file://hello.com/image/jpg")
|
||||||
|
|
||||||
|
with self.assertRaises(ConnectorException):
|
||||||
|
get_data("http://127.0.0.1/image/jpg")
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue