mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-27 03:51:08 +00:00
Merge branch 'main' into book-file-links
This commit is contained in:
commit
16a58ae079
262 changed files with 13490 additions and 5262 deletions
|
@ -1,75 +0,0 @@
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
|
||||||
SECRET_KEY="7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr"
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
|
||||||
DEBUG=true
|
|
||||||
USE_HTTPS=false
|
|
||||||
|
|
||||||
DOMAIN=your.domain.here
|
|
||||||
#EMAIL=your@email.here
|
|
||||||
|
|
||||||
# Used for deciding which editions to prefer
|
|
||||||
DEFAULT_LANGUAGE="English"
|
|
||||||
|
|
||||||
## Leave unset to allow all hosts
|
|
||||||
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
|
|
||||||
|
|
||||||
MEDIA_ROOT=images/
|
|
||||||
|
|
||||||
PGPORT=5432
|
|
||||||
POSTGRES_PASSWORD=securedbypassword123
|
|
||||||
POSTGRES_USER=fedireads
|
|
||||||
POSTGRES_DB=fedireads
|
|
||||||
POSTGRES_HOST=db
|
|
||||||
|
|
||||||
# Redis activity stream manager
|
|
||||||
MAX_STREAM_LENGTH=200
|
|
||||||
REDIS_ACTIVITY_HOST=redis_activity
|
|
||||||
REDIS_ACTIVITY_PORT=6379
|
|
||||||
#REDIS_ACTIVITY_PASSWORD=redispassword345
|
|
||||||
|
|
||||||
# Redis as celery broker
|
|
||||||
REDIS_BROKER_PORT=6379
|
|
||||||
#REDIS_BROKER_PASSWORD=redispassword123
|
|
||||||
|
|
||||||
FLOWER_PORT=8888
|
|
||||||
#FLOWER_USER=mouse
|
|
||||||
#FLOWER_PASSWORD=changeme
|
|
||||||
|
|
||||||
EMAIL_HOST=smtp.mailgun.org
|
|
||||||
EMAIL_PORT=587
|
|
||||||
EMAIL_HOST_USER=mail@your.domain.here
|
|
||||||
EMAIL_HOST_PASSWORD=emailpassword123
|
|
||||||
EMAIL_USE_TLS=true
|
|
||||||
EMAIL_USE_SSL=false
|
|
||||||
|
|
||||||
# Thumbnails Generation
|
|
||||||
ENABLE_THUMBNAIL_GENERATION=false
|
|
||||||
|
|
||||||
# S3 configuration
|
|
||||||
USE_S3=false
|
|
||||||
AWS_ACCESS_KEY_ID=
|
|
||||||
AWS_SECRET_ACCESS_KEY=
|
|
||||||
|
|
||||||
# Commented are example values if you use a non-AWS, S3-compatible service
|
|
||||||
# AWS S3 should work with only AWS_STORAGE_BUCKET_NAME and AWS_S3_REGION_NAME
|
|
||||||
# non-AWS S3-compatible services will need AWS_STORAGE_BUCKET_NAME,
|
|
||||||
# along with both AWS_S3_CUSTOM_DOMAIN and AWS_S3_ENDPOINT_URL
|
|
||||||
|
|
||||||
# AWS_STORAGE_BUCKET_NAME= # "example-bucket-name"
|
|
||||||
# AWS_S3_CUSTOM_DOMAIN=None # "example-bucket-name.s3.fr-par.scw.cloud"
|
|
||||||
# AWS_S3_REGION_NAME=None # "fr-par"
|
|
||||||
# AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud"
|
|
||||||
|
|
||||||
|
|
||||||
# Preview image generation can be computing and storage intensive
|
|
||||||
# ENABLE_PREVIEW_IMAGES=True
|
|
||||||
|
|
||||||
# Specify RGB tuple or RGB hex strings,
|
|
||||||
# or use_dominant_color_light / use_dominant_color_dark
|
|
||||||
PREVIEW_BG_COLOR=use_dominant_color_light
|
|
||||||
# Change to #FFF if you use use_dominant_color_dark
|
|
||||||
PREVIEW_TEXT_COLOR=#363636
|
|
||||||
PREVIEW_IMG_WIDTH=1200
|
|
||||||
PREVIEW_IMG_HEIGHT=630
|
|
||||||
PREVIEW_DEFAULT_COVER_COLOR=#002549
|
|
|
@ -16,6 +16,7 @@ DEFAULT_LANGUAGE="English"
|
||||||
|
|
||||||
MEDIA_ROOT=images/
|
MEDIA_ROOT=images/
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
PGPORT=5432
|
PGPORT=5432
|
||||||
POSTGRES_PASSWORD=securedbypassword123
|
POSTGRES_PASSWORD=securedbypassword123
|
||||||
POSTGRES_USER=fedireads
|
POSTGRES_USER=fedireads
|
||||||
|
@ -32,16 +33,25 @@ REDIS_ACTIVITY_PASSWORD=redispassword345
|
||||||
REDIS_BROKER_PORT=6379
|
REDIS_BROKER_PORT=6379
|
||||||
REDIS_BROKER_PASSWORD=redispassword123
|
REDIS_BROKER_PASSWORD=redispassword123
|
||||||
|
|
||||||
|
# Monitoring for celery
|
||||||
FLOWER_PORT=8888
|
FLOWER_PORT=8888
|
||||||
FLOWER_USER=mouse
|
FLOWER_USER=mouse
|
||||||
FLOWER_PASSWORD=changeme
|
FLOWER_PASSWORD=changeme
|
||||||
|
|
||||||
|
# Email config
|
||||||
EMAIL_HOST=smtp.mailgun.org
|
EMAIL_HOST=smtp.mailgun.org
|
||||||
EMAIL_PORT=587
|
EMAIL_PORT=587
|
||||||
EMAIL_HOST_USER=mail@your.domain.here
|
EMAIL_HOST_USER=mail@your.domain.here
|
||||||
EMAIL_HOST_PASSWORD=emailpassword123
|
EMAIL_HOST_PASSWORD=emailpassword123
|
||||||
EMAIL_USE_TLS=true
|
EMAIL_USE_TLS=true
|
||||||
EMAIL_USE_SSL=false
|
EMAIL_USE_SSL=false
|
||||||
|
EMAIL_SENDER_NAME=admin
|
||||||
|
# defaults to DOMAIN
|
||||||
|
EMAIL_SENDER_DOMAIN=
|
||||||
|
|
||||||
|
# Query timeouts
|
||||||
|
SEARCH_TIMEOUT=15
|
||||||
|
QUERY_TIMEOUT=5
|
||||||
|
|
||||||
# Thumbnails Generation
|
# Thumbnails Generation
|
||||||
ENABLE_THUMBNAIL_GENERATION=false
|
ENABLE_THUMBNAIL_GENERATION=false
|
2
.github/workflows/django-tests.yml
vendored
2
.github/workflows/django-tests.yml
vendored
|
@ -46,6 +46,8 @@ jobs:
|
||||||
POSTGRES_HOST: 127.0.0.1
|
POSTGRES_HOST: 127.0.0.1
|
||||||
CELERY_BROKER: ""
|
CELERY_BROKER: ""
|
||||||
REDIS_BROKER_PORT: 6379
|
REDIS_BROKER_PORT: 6379
|
||||||
|
REDIS_BROKER_PASSWORD: beep
|
||||||
|
USE_DUMMY_CACHE: true
|
||||||
FLOWER_PORT: 8888
|
FLOWER_PORT: 8888
|
||||||
EMAIL_HOST: "smtp.mailgun.org"
|
EMAIL_HOST: "smtp.mailgun.org"
|
||||||
EMAIL_PORT: 587
|
EMAIL_PORT: 587
|
||||||
|
|
24
.github/workflows/prettier.yaml
vendored
Normal file
24
.github/workflows/prettier.yaml
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# @url https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||||
|
name: JavaScript Prettier (run ./bw-dev prettier to fix)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint with Prettier
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it.
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install modules
|
||||||
|
run: npm install .
|
||||||
|
|
||||||
|
# See .stylelintignore for files that are not linted.
|
||||||
|
- name: Run Prettier
|
||||||
|
run: npx prettier --check bookwyrm/static/js/*.js
|
|
@ -12,6 +12,9 @@ module.exports = {
|
||||||
"custom-properties",
|
"custom-properties",
|
||||||
"declarations"
|
"declarations"
|
||||||
],
|
],
|
||||||
"indentation": 4
|
"indentation": 4,
|
||||||
|
"property-no-vendor-prefix": null,
|
||||||
|
"color-function-notation": null,
|
||||||
|
"declaration-block-no-redundant-longhand-properties": null,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -397,9 +397,15 @@ def populate_streams_on_account_create(sender, instance, created, *args, **kwarg
|
||||||
"""build a user's feeds when they join"""
|
"""build a user's feeds when they join"""
|
||||||
if not created or not instance.local:
|
if not created or not instance.local:
|
||||||
return
|
return
|
||||||
|
transaction.on_commit(
|
||||||
|
lambda: populate_streams_on_account_create_command(instance.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def populate_streams_on_account_create_command(instance_id):
|
||||||
|
"""wait for the transaction to complete"""
|
||||||
for stream in streams:
|
for stream in streams:
|
||||||
populate_stream_task.delay(stream, instance.id)
|
populate_stream_task.delay(stream, instance_id)
|
||||||
|
|
||||||
|
|
||||||
@receiver(signals.pre_save, sender=models.ShelfBook)
|
@receiver(signals.pre_save, sender=models.ShelfBook)
|
||||||
|
|
|
@ -35,7 +35,7 @@ class AbstractMinimalConnector(ABC):
|
||||||
for field in self_fields:
|
for field in self_fields:
|
||||||
setattr(self, field, getattr(info, field))
|
setattr(self, field, getattr(info, field))
|
||||||
|
|
||||||
def search(self, query, min_confidence=None, timeout=5):
|
def search(self, query, min_confidence=None, timeout=settings.QUERY_TIMEOUT):
|
||||||
"""free text search"""
|
"""free text search"""
|
||||||
params = {}
|
params = {}
|
||||||
if min_confidence:
|
if min_confidence:
|
||||||
|
@ -52,12 +52,13 @@ class AbstractMinimalConnector(ABC):
|
||||||
results.append(self.format_search_result(doc))
|
results.append(self.format_search_result(doc))
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def isbn_search(self, query):
|
def isbn_search(self, query, timeout=settings.QUERY_TIMEOUT):
|
||||||
"""isbn search"""
|
"""isbn search"""
|
||||||
params = {}
|
params = {}
|
||||||
data = self.get_search_data(
|
data = self.get_search_data(
|
||||||
f"{self.isbn_search_url}{query}",
|
f"{self.isbn_search_url}{query}",
|
||||||
params=params,
|
params=params,
|
||||||
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ from django.db.models import signals
|
||||||
from requests import HTTPError
|
from requests import HTTPError
|
||||||
|
|
||||||
from bookwyrm import book_search, models
|
from bookwyrm import book_search, models
|
||||||
|
from bookwyrm.settings import SEARCH_TIMEOUT
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -30,7 +31,6 @@ def search(query, min_confidence=0.1, return_first=False):
|
||||||
isbn = re.sub(r"[\W_]", "", query)
|
isbn = re.sub(r"[\W_]", "", query)
|
||||||
maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13
|
maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13
|
||||||
|
|
||||||
timeout = 15
|
|
||||||
start_time = datetime.now()
|
start_time = datetime.now()
|
||||||
for connector in get_connectors():
|
for connector in get_connectors():
|
||||||
result_set = None
|
result_set = None
|
||||||
|
@ -62,7 +62,7 @@ def search(query, min_confidence=0.1, return_first=False):
|
||||||
"results": result_set,
|
"results": result_set,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (datetime.now() - start_time).seconds >= timeout:
|
if (datetime.now() - start_time).seconds >= SEARCH_TIMEOUT:
|
||||||
break
|
break
|
||||||
|
|
||||||
if return_first:
|
if return_first:
|
||||||
|
|
|
@ -69,7 +69,7 @@ def format_email(email_name, data):
|
||||||
def send_email(recipient, subject, html_content, text_content):
|
def send_email(recipient, subject, html_content, text_content):
|
||||||
"""use a task to send the email"""
|
"""use a task to send the email"""
|
||||||
email = EmailMultiAlternatives(
|
email = EmailMultiAlternatives(
|
||||||
subject, text_content, settings.DEFAULT_FROM_EMAIL, [recipient]
|
subject, text_content, settings.EMAIL_SENDER, [recipient]
|
||||||
)
|
)
|
||||||
email.attach_alternative(html_content, "text/html")
|
email.attach_alternative(html_content, "text/html")
|
||||||
email.send()
|
email.send()
|
||||||
|
|
|
@ -14,7 +14,8 @@ class LibrarythingImporter(Importer):
|
||||||
"""use the dataclass to create the formatted row of data"""
|
"""use the dataclass to create the formatted row of data"""
|
||||||
remove_brackets = lambda v: re.sub(r"\[|\]", "", v) if v else None
|
remove_brackets = lambda v: re.sub(r"\[|\]", "", v) if v else None
|
||||||
normalized = {k: remove_brackets(entry.get(v)) for k, v in mappings.items()}
|
normalized = {k: remove_brackets(entry.get(v)) for k, v in mappings.items()}
|
||||||
isbn_13 = normalized["isbn_13"].split(", ")
|
isbn_13 = normalized.get("isbn_13")
|
||||||
|
isbn_13 = isbn_13.split(", ") if isbn_13 else []
|
||||||
normalized["isbn_13"] = isbn_13[1] if len(isbn_13) > 0 else None
|
normalized["isbn_13"] = isbn_13[1] if len(isbn_13) > 0 else None
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
251
bookwyrm/lists_stream.py
Normal file
251
bookwyrm/lists_stream.py
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
""" access the list streams stored in redis """
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import signals, Count, Q
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
from bookwyrm.redis_store import RedisStore
|
||||||
|
from bookwyrm.tasks import app, MEDIUM, HIGH
|
||||||
|
|
||||||
|
|
||||||
|
class ListsStream(RedisStore):
|
||||||
|
"""all the lists you can see"""
|
||||||
|
|
||||||
|
def stream_id(self, user): # pylint: disable=no-self-use
|
||||||
|
"""the redis key for this user's instance of this stream"""
|
||||||
|
if isinstance(user, int):
|
||||||
|
# allows the function to take an int or an obj
|
||||||
|
return f"{user}-lists"
|
||||||
|
return f"{user.id}-lists"
|
||||||
|
|
||||||
|
def get_rank(self, obj): # pylint: disable=no-self-use
|
||||||
|
"""lists are sorted by updated date"""
|
||||||
|
return obj.updated_date.timestamp()
|
||||||
|
|
||||||
|
def add_list(self, book_list):
|
||||||
|
"""add a list to users' feeds"""
|
||||||
|
# the pipeline contains all the add-to-stream activities
|
||||||
|
self.add_object_to_related_stores(book_list)
|
||||||
|
|
||||||
|
def add_user_lists(self, viewer, user):
|
||||||
|
"""add a user's lists to another user's feed"""
|
||||||
|
# only add the lists that the viewer should be able to see
|
||||||
|
lists = models.List.privacy_filter(viewer).filter(user=user)
|
||||||
|
self.bulk_add_objects_to_store(lists, self.stream_id(viewer))
|
||||||
|
|
||||||
|
def remove_user_lists(self, viewer, user, exclude_privacy=None):
|
||||||
|
"""remove a user's list from another user's feed"""
|
||||||
|
# remove all so that followers only lists are removed
|
||||||
|
lists = user.list_set
|
||||||
|
if exclude_privacy:
|
||||||
|
lists = lists.exclude(privacy=exclude_privacy)
|
||||||
|
self.bulk_remove_objects_from_store(lists.all(), self.stream_id(viewer))
|
||||||
|
|
||||||
|
def get_list_stream(self, user):
|
||||||
|
"""load the lists to be displayed"""
|
||||||
|
lists = self.get_store(self.stream_id(user))
|
||||||
|
return (
|
||||||
|
models.List.objects.filter(id__in=lists)
|
||||||
|
.annotate(item_count=Count("listitem", filter=Q(listitem__approved=True)))
|
||||||
|
# hide lists with no approved books
|
||||||
|
.filter(item_count__gt=0)
|
||||||
|
.select_related("user")
|
||||||
|
.prefetch_related("listitem_set")
|
||||||
|
.order_by("-updated_date")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
def populate_lists(self, user):
|
||||||
|
"""go from zero to a timeline"""
|
||||||
|
self.populate_store(self.stream_id(user))
|
||||||
|
|
||||||
|
def get_audience(self, book_list): # pylint: disable=no-self-use
|
||||||
|
"""given a list, what users should see it"""
|
||||||
|
# everybody who could plausibly see this list
|
||||||
|
audience = models.User.objects.filter(
|
||||||
|
is_active=True,
|
||||||
|
local=True, # we only create feeds for users of this instance
|
||||||
|
).exclude( # not blocked
|
||||||
|
Q(id__in=book_list.user.blocks.all()) | Q(blocks=book_list.user)
|
||||||
|
)
|
||||||
|
|
||||||
|
group = book_list.group
|
||||||
|
# only visible to the poster and mentioned users
|
||||||
|
if book_list.privacy == "direct":
|
||||||
|
if group:
|
||||||
|
audience = audience.filter(
|
||||||
|
Q(id=book_list.user.id) # if the user is the post's author
|
||||||
|
| ~Q(groups=group.memberships) # if the user is in the group
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
audience = audience.filter(
|
||||||
|
Q(id=book_list.user.id) # if the user is the post's author
|
||||||
|
)
|
||||||
|
# only visible to the poster's followers and tagged users
|
||||||
|
elif book_list.privacy == "followers":
|
||||||
|
if group:
|
||||||
|
audience = audience.filter(
|
||||||
|
Q(id=book_list.user.id) # if the user is the list's owner
|
||||||
|
| Q(following=book_list.user) # if the user is following the pwmer
|
||||||
|
# if a user is in the group
|
||||||
|
| Q(memberships__group__id=book_list.group.id)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
audience = audience.filter(
|
||||||
|
Q(id=book_list.user.id) # if the user is the list's owner
|
||||||
|
| Q(following=book_list.user) # if the user is following the pwmer
|
||||||
|
)
|
||||||
|
return audience.distinct()
|
||||||
|
|
||||||
|
def get_stores_for_object(self, obj):
|
||||||
|
return [self.stream_id(u) for u in self.get_audience(obj)]
|
||||||
|
|
||||||
|
def get_lists_for_user(self, user): # pylint: disable=no-self-use
|
||||||
|
"""given a user, what lists should they see on this stream"""
|
||||||
|
return models.List.privacy_filter(
|
||||||
|
user,
|
||||||
|
privacy_levels=["public", "followers"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_objects_for_store(self, store):
|
||||||
|
user = models.User.objects.get(id=store.split("-")[0])
|
||||||
|
return self.get_lists_for_user(user)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(signals.post_save, sender=models.List)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def add_list_on_create(sender, instance, created, *args, **kwargs):
|
||||||
|
"""add newly created lists streamsstreams"""
|
||||||
|
if not created:
|
||||||
|
return
|
||||||
|
# when creating new things, gotta wait on the transaction
|
||||||
|
transaction.on_commit(lambda: add_list_on_create_command(instance.id))
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(signals.post_delete, sender=models.List)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def remove_list_on_delete(sender, instance, *args, **kwargs):
|
||||||
|
"""remove deleted lists to streams"""
|
||||||
|
remove_list_task.delay(instance.id)
|
||||||
|
|
||||||
|
|
||||||
|
def add_list_on_create_command(instance_id):
|
||||||
|
"""runs this code only after the database commit completes"""
|
||||||
|
add_list_task.delay(instance_id)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(signals.post_save, sender=models.UserFollows)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def add_lists_on_follow(sender, instance, created, *args, **kwargs):
|
||||||
|
"""add a newly followed user's lists to feeds"""
|
||||||
|
if not created or not instance.user_subject.local:
|
||||||
|
return
|
||||||
|
add_user_lists_task.delay(instance.user_subject.id, instance.user_object.id)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(signals.post_delete, sender=models.UserFollows)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def remove_lists_on_unfollow(sender, instance, *args, **kwargs):
|
||||||
|
"""remove lists from a feed on unfollow"""
|
||||||
|
if not instance.user_subject.local:
|
||||||
|
return
|
||||||
|
# remove all but public lists
|
||||||
|
remove_user_lists_task.delay(
|
||||||
|
instance.user_subject.id, instance.user_object.id, exclude_privacy="public"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(signals.post_save, sender=models.UserBlocks)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def remove_lists_on_block(sender, instance, *args, **kwargs):
|
||||||
|
"""remove lists from all feeds on block"""
|
||||||
|
# blocks apply ot all feeds
|
||||||
|
if instance.user_subject.local:
|
||||||
|
remove_user_lists_task.delay(instance.user_subject.id, instance.user_object.id)
|
||||||
|
|
||||||
|
# and in both directions
|
||||||
|
if instance.user_object.local:
|
||||||
|
remove_user_lists_task.delay(instance.user_object.id, instance.user_subject.id)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(signals.post_delete, sender=models.UserBlocks)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def add_lists_on_unblock(sender, instance, *args, **kwargs):
|
||||||
|
"""add lists back to all feeds on unblock"""
|
||||||
|
# make sure there isn't a block in the other direction
|
||||||
|
if models.UserBlocks.objects.filter(
|
||||||
|
user_subject=instance.user_object,
|
||||||
|
user_object=instance.user_subject,
|
||||||
|
).exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
# add lists back to streams with lists from anyone
|
||||||
|
if instance.user_subject.local:
|
||||||
|
add_user_lists_task.delay(
|
||||||
|
instance.user_subject.id,
|
||||||
|
instance.user_object.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# add lists back to streams with lists from anyone
|
||||||
|
if instance.user_object.local:
|
||||||
|
add_user_lists_task.delay(
|
||||||
|
instance.user_object.id,
|
||||||
|
instance.user_subject.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(signals.post_save, sender=models.User)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def populate_lists_on_account_create(sender, instance, created, *args, **kwargs):
|
||||||
|
"""build a user's feeds when they join"""
|
||||||
|
if not created or not instance.local:
|
||||||
|
return
|
||||||
|
transaction.on_commit(lambda: add_list_on_account_create_command(instance.id))
|
||||||
|
|
||||||
|
|
||||||
|
def add_list_on_account_create_command(user_id):
|
||||||
|
"""wait for the transaction to complete"""
|
||||||
|
populate_lists_task.delay(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- TASKS
|
||||||
|
@app.task(queue=MEDIUM)
|
||||||
|
def populate_lists_task(user_id):
|
||||||
|
"""background task for populating an empty list stream"""
|
||||||
|
user = models.User.objects.get(id=user_id)
|
||||||
|
ListsStream().populate_lists(user)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(queue=MEDIUM)
|
||||||
|
def remove_list_task(list_id):
|
||||||
|
"""remove a list from any stream it might be in"""
|
||||||
|
stores = models.User.objects.filter(local=True, is_active=True).values_list(
|
||||||
|
"id", flat=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# delete for every store
|
||||||
|
stores = [ListsStream().stream_id(idx) for idx in stores]
|
||||||
|
ListsStream().remove_object_from_related_stores(list_id, stores=stores)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(queue=HIGH)
|
||||||
|
def add_list_task(list_id):
|
||||||
|
"""add a list to any stream it should be in"""
|
||||||
|
book_list = models.List.objects.get(id=list_id)
|
||||||
|
ListsStream().add_list(book_list)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(queue=MEDIUM)
|
||||||
|
def remove_user_lists_task(viewer_id, user_id, exclude_privacy=None):
|
||||||
|
"""remove all lists by a user from a viewer's stream"""
|
||||||
|
viewer = models.User.objects.get(id=viewer_id)
|
||||||
|
user = models.User.objects.get(id=user_id)
|
||||||
|
ListsStream().remove_user_lists(viewer, user, exclude_privacy=exclude_privacy)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(queue=MEDIUM)
|
||||||
|
def add_user_lists_task(viewer_id, user_id):
|
||||||
|
"""add all lists by a user to a viewer's stream"""
|
||||||
|
viewer = models.User.objects.get(id=viewer_id)
|
||||||
|
user = models.User.objects.get(id=user_id)
|
||||||
|
ListsStream().add_user_lists(viewer, user)
|
34
bookwyrm/management/commands/populate_lists_streams.py
Normal file
34
bookwyrm/management/commands/populate_lists_streams.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
""" Re-create list streams """
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from bookwyrm import lists_stream, models
|
||||||
|
|
||||||
|
|
||||||
|
def populate_lists_streams():
|
||||||
|
"""build all the lists streams for all the users"""
|
||||||
|
print("Populating lists streams")
|
||||||
|
users = models.User.objects.filter(
|
||||||
|
local=True,
|
||||||
|
is_active=True,
|
||||||
|
).order_by("-last_active_date")
|
||||||
|
print("This may take a long time! Please be patient.")
|
||||||
|
for user in users:
|
||||||
|
print(".", end="")
|
||||||
|
lists_stream.populate_lists_task.delay(user.id)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""start all over with lists streams"""
|
||||||
|
|
||||||
|
help = "Populate list streams for all users"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--stream",
|
||||||
|
default=None,
|
||||||
|
help="Specifies which time of stream to populate",
|
||||||
|
)
|
||||||
|
|
||||||
|
# pylint: disable=no-self-use,unused-argument
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
"""run feed builder"""
|
||||||
|
populate_lists_streams()
|
|
@ -1,18 +1,20 @@
|
||||||
""" Re-create user streams """
|
""" Re-create user streams """
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from bookwyrm import activitystreams, models
|
from bookwyrm import activitystreams, lists_stream, models
|
||||||
|
|
||||||
|
|
||||||
def populate_streams(stream=None):
|
def populate_streams(stream=None):
|
||||||
"""build all the streams for all the users"""
|
"""build all the streams for all the users"""
|
||||||
streams = [stream] if stream else activitystreams.streams.keys()
|
streams = [stream] if stream else activitystreams.streams.keys()
|
||||||
print("Populations streams", streams)
|
print("Populating streams", streams)
|
||||||
users = models.User.objects.filter(
|
users = models.User.objects.filter(
|
||||||
local=True,
|
local=True,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
).order_by("-last_active_date")
|
).order_by("-last_active_date")
|
||||||
print("This may take a long time! Please be patient.")
|
print("This may take a long time! Please be patient.")
|
||||||
for user in users:
|
for user in users:
|
||||||
|
print(".", end="")
|
||||||
|
lists_stream.populate_lists_task.delay(user.id)
|
||||||
for stream_key in streams:
|
for stream_key in streams:
|
||||||
print(".", end="")
|
print(".", end="")
|
||||||
activitystreams.populate_stream_task.delay(stream_key, user.id)
|
activitystreams.populate_stream_task.delay(stream_key, user.id)
|
||||||
|
|
18
bookwyrm/migrations/0121_user_summary_keys.py
Normal file
18
bookwyrm/migrations/0121_user_summary_keys.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.5 on 2021-12-22 11:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0120_list_embed_key"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="summary_keys",
|
||||||
|
field=models.JSONField(null=True),
|
||||||
|
),
|
||||||
|
]
|
19
bookwyrm/migrations/0122_alter_annualgoal_year.py
Normal file
19
bookwyrm/migrations/0122_alter_annualgoal_year.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 3.2.5 on 2022-01-04 18:59
|
||||||
|
|
||||||
|
import bookwyrm.models.user
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0121_user_summary_keys"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="annualgoal",
|
||||||
|
name="year",
|
||||||
|
field=models.IntegerField(default=bookwyrm.models.user.get_current_year),
|
||||||
|
),
|
||||||
|
]
|
34
bookwyrm/migrations/0123_alter_user_preferred_language.py
Normal file
34
bookwyrm/migrations/0123_alter_user_preferred_language.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# Generated by Django 3.2.5 on 2022-01-04 22:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0122_alter_annualgoal_year"),
|
||||||
|
]
|
||||||
|
|
||||||
|
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)"),
|
||||||
|
("fr-fr", "Français (French)"),
|
||||||
|
("lt-lt", "Lietuvių (Lithuanian)"),
|
||||||
|
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
||||||
|
("pt-pt", "Português Europeu (European Portuguese)"),
|
||||||
|
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||||
|
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||||
|
],
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
33
bookwyrm/migrations/0124_auto_20220106_1759.py
Normal file
33
bookwyrm/migrations/0124_auto_20220106_1759.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 3.2.10 on 2022-01-06 17:59
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def get_admins(apps, schema_editor):
|
||||||
|
"""add any superusers to the "admin" group"""
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
groups = apps.get_model("auth", "Group")
|
||||||
|
try:
|
||||||
|
group = groups.objects.using(db_alias).get(name="admin")
|
||||||
|
except groups.DoesNotExist:
|
||||||
|
# for tests
|
||||||
|
return
|
||||||
|
|
||||||
|
users = apps.get_model("bookwyrm", "User")
|
||||||
|
admins = users.objects.using(db_alias).filter(is_superuser=True)
|
||||||
|
|
||||||
|
for admin in admins:
|
||||||
|
admin.groups.add(group)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0123_alter_user_preferred_language"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(get_admins, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
|
@ -1,6 +1,8 @@
|
||||||
""" database schema for info about authors """
|
""" database schema for info about authors """
|
||||||
import re
|
import re
|
||||||
from django.contrib.postgres.indexes import GinIndex
|
from django.contrib.postgres.indexes import GinIndex
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.cache.utils import make_template_fragment_key
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
|
@ -34,6 +36,17 @@ class Author(BookDataModel):
|
||||||
)
|
)
|
||||||
bio = fields.HtmlField(null=True, blank=True)
|
bio = fields.HtmlField(null=True, blank=True)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""clear related template caches"""
|
||||||
|
# clear template caches
|
||||||
|
if self.id:
|
||||||
|
cache_keys = [
|
||||||
|
make_template_fragment_key("titleby", [book])
|
||||||
|
for book in self.book_set.values_list("id", flat=True)
|
||||||
|
]
|
||||||
|
cache.delete_many(cache_keys)
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def isni_link(self):
|
def isni_link(self):
|
||||||
"""generate the url from the isni id"""
|
"""generate the url from the isni id"""
|
||||||
|
|
|
@ -84,6 +84,7 @@ class BookWyrmModel(models.Model):
|
||||||
# you can see groups of which you are a member
|
# you can see groups of which you are a member
|
||||||
if (
|
if (
|
||||||
hasattr(self, "memberships")
|
hasattr(self, "memberships")
|
||||||
|
and viewer.is_authenticated
|
||||||
and self.memberships.filter(user=viewer).exists()
|
and self.memberships.filter(user=viewer).exists()
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
|
@ -3,6 +3,8 @@ import re
|
||||||
|
|
||||||
from django.contrib.postgres.search import SearchVectorField
|
from django.contrib.postgres.search import SearchVectorField
|
||||||
from django.contrib.postgres.indexes import GinIndex
|
from django.contrib.postgres.indexes import GinIndex
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.cache.utils import make_template_fragment_key
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
@ -185,6 +187,11 @@ class Book(BookDataModel):
|
||||||
"""can't be abstract for query reasons, but you shouldn't USE it"""
|
"""can't be abstract for query reasons, but you shouldn't USE it"""
|
||||||
if not isinstance(self, Edition) and not isinstance(self, Work):
|
if not isinstance(self, Edition) and not isinstance(self, Work):
|
||||||
raise ValueError("Books should be added as Editions or Works")
|
raise ValueError("Books should be added as Editions or Works")
|
||||||
|
|
||||||
|
# clear template caches
|
||||||
|
cache_key = make_template_fragment_key("titleby", [self.id])
|
||||||
|
cache.delete(cache_key)
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
""" defines relationships between users """
|
""" defines relationships between users """
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.cache.utils import make_template_fragment_key
|
||||||
from django.db import models, transaction, IntegrityError
|
from django.db import models, transaction, IntegrityError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
|
@ -36,6 +38,20 @@ class UserRelationship(BookWyrmModel):
|
||||||
"""the remote user needs to recieve direct broadcasts"""
|
"""the remote user needs to recieve direct broadcasts"""
|
||||||
return [u for u in [self.user_subject, self.user_object] if not u.local]
|
return [u for u in [self.user_subject, self.user_object] if not u.local]
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""clear the template cache"""
|
||||||
|
# invalidate the template cache
|
||||||
|
cache_keys = [
|
||||||
|
make_template_fragment_key(
|
||||||
|
"follow_button", [self.user_subject.id, self.user_object.id]
|
||||||
|
),
|
||||||
|
make_template_fragment_key(
|
||||||
|
"follow_button", [self.user_object.id, self.user_subject.id]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
cache.delete_many(cache_keys)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""relationships should be unique"""
|
"""relationships should be unique"""
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
|
|
||||||
if not self.reply_parent:
|
if not self.reply_parent:
|
||||||
self.thread_id = self.id
|
self.thread_id = self.id
|
||||||
|
|
||||||
super().save(broadcast=False, update_fields=["thread_id"])
|
super().save(broadcast=False, update_fields=["thread_id"])
|
||||||
|
|
||||||
def delete(self, *args, **kwargs): # pylint: disable=unused-argument
|
def delete(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||||
|
|
|
@ -148,6 +148,8 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
size=8,
|
size=8,
|
||||||
default=get_feed_filter_choices,
|
default=get_feed_filter_choices,
|
||||||
)
|
)
|
||||||
|
# annual summary keys
|
||||||
|
summary_keys = models.JSONField(null=True)
|
||||||
|
|
||||||
preferred_timezone = models.CharField(
|
preferred_timezone = models.CharField(
|
||||||
choices=[(str(tz), str(tz)) for tz in pytz.all_timezones],
|
choices=[(str(tz), str(tz)) for tz in pytz.all_timezones],
|
||||||
|
@ -406,12 +408,17 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_year():
|
||||||
|
"""sets default year for annual goal to this year"""
|
||||||
|
return timezone.now().year
|
||||||
|
|
||||||
|
|
||||||
class AnnualGoal(BookWyrmModel):
|
class AnnualGoal(BookWyrmModel):
|
||||||
"""set a goal for how many books you read in a year"""
|
"""set a goal for how many books you read in a year"""
|
||||||
|
|
||||||
user = models.ForeignKey("User", on_delete=models.PROTECT)
|
user = models.ForeignKey("User", on_delete=models.PROTECT)
|
||||||
goal = models.IntegerField(validators=[MinValueValidator(1)])
|
goal = models.IntegerField(validators=[MinValueValidator(1)])
|
||||||
year = models.IntegerField(default=timezone.now().year)
|
year = models.IntegerField(default=get_current_year)
|
||||||
privacy = models.CharField(
|
privacy = models.CharField(
|
||||||
max_length=255, default="public", choices=fields.PrivacyLevels.choices
|
max_length=255, default="public", choices=fields.PrivacyLevels.choices
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,10 @@ import redis
|
||||||
from bookwyrm import settings
|
from bookwyrm import settings
|
||||||
|
|
||||||
r = redis.Redis(
|
r = redis.Redis(
|
||||||
host=settings.REDIS_ACTIVITY_HOST, port=settings.REDIS_ACTIVITY_PORT, db=0
|
host=settings.REDIS_ACTIVITY_HOST,
|
||||||
|
port=settings.REDIS_ACTIVITY_PORT,
|
||||||
|
password=settings.REDIS_ACTIVITY_PASSWORD,
|
||||||
|
db=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,7 +30,8 @@ class RedisStore(ABC):
|
||||||
# add the status to the feed
|
# add the status to the feed
|
||||||
pipeline.zadd(store, value)
|
pipeline.zadd(store, value)
|
||||||
# trim the store
|
# trim the store
|
||||||
pipeline.zremrangebyrank(store, 0, -1 * self.max_length)
|
if self.max_length:
|
||||||
|
pipeline.zremrangebyrank(store, 0, -1 * self.max_length)
|
||||||
if not execute:
|
if not execute:
|
||||||
return pipeline
|
return pipeline
|
||||||
# and go!
|
# and go!
|
||||||
|
@ -35,10 +39,15 @@ class RedisStore(ABC):
|
||||||
|
|
||||||
def remove_object_from_related_stores(self, obj, stores=None):
|
def remove_object_from_related_stores(self, obj, stores=None):
|
||||||
"""remove an object from all stores"""
|
"""remove an object from all stores"""
|
||||||
|
# if the stoers are provided, the object can just be an id
|
||||||
|
if stores and isinstance(obj, int):
|
||||||
|
obj_id = obj
|
||||||
|
else:
|
||||||
|
obj_id = obj.id
|
||||||
stores = self.get_stores_for_object(obj) if stores is None else stores
|
stores = self.get_stores_for_object(obj) if stores is None else stores
|
||||||
pipeline = r.pipeline()
|
pipeline = r.pipeline()
|
||||||
for store in stores:
|
for store in stores:
|
||||||
pipeline.zrem(store, -1, obj.id)
|
pipeline.zrem(store, -1, obj_id)
|
||||||
pipeline.execute()
|
pipeline.execute()
|
||||||
|
|
||||||
def bulk_add_objects_to_store(self, objs, store):
|
def bulk_add_objects_to_store(self, objs, store):
|
||||||
|
@ -46,7 +55,7 @@ class RedisStore(ABC):
|
||||||
pipeline = r.pipeline()
|
pipeline = r.pipeline()
|
||||||
for obj in objs[: self.max_length]:
|
for obj in objs[: self.max_length]:
|
||||||
pipeline.zadd(store, self.get_value(obj))
|
pipeline.zadd(store, self.get_value(obj))
|
||||||
if objs:
|
if objs and self.max_length:
|
||||||
pipeline.zremrangebyrank(store, 0, -1 * self.max_length)
|
pipeline.zremrangebyrank(store, 0, -1 * self.max_length)
|
||||||
pipeline.execute()
|
pipeline.execute()
|
||||||
|
|
||||||
|
@ -70,7 +79,7 @@ class RedisStore(ABC):
|
||||||
pipeline.zadd(store, self.get_value(obj))
|
pipeline.zadd(store, self.get_value(obj))
|
||||||
|
|
||||||
# only trim the store if objects were added
|
# only trim the store if objects were added
|
||||||
if queryset.exists():
|
if queryset.exists() and self.max_length:
|
||||||
pipeline.zremrangebyrank(store, 0, -1 * self.max_length)
|
pipeline.zremrangebyrank(store, 0, -1 * self.max_length)
|
||||||
pipeline.execute()
|
pipeline.execute()
|
||||||
|
|
||||||
|
|
|
@ -9,12 +9,12 @@ from django.utils.translation import gettext_lazy as _
|
||||||
env = Env()
|
env = Env()
|
||||||
env.read_env()
|
env.read_env()
|
||||||
DOMAIN = env("DOMAIN")
|
DOMAIN = env("DOMAIN")
|
||||||
VERSION = "0.1.1"
|
VERSION = "0.1.2"
|
||||||
|
|
||||||
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 = "3891b373"
|
JS_CACHE = "2d3181e1"
|
||||||
|
|
||||||
# email
|
# email
|
||||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||||
|
@ -24,7 +24,9 @@ EMAIL_HOST_USER = env("EMAIL_HOST_USER")
|
||||||
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
|
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
|
||||||
EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", True)
|
EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", True)
|
||||||
EMAIL_USE_SSL = env.bool("EMAIL_USE_SSL", False)
|
EMAIL_USE_SSL = env.bool("EMAIL_USE_SSL", False)
|
||||||
DEFAULT_FROM_EMAIL = f"admin@{DOMAIN}"
|
EMAIL_SENDER_NAME = env("EMAIL_SENDER_NAME", "admin")
|
||||||
|
EMAIL_SENDER_DOMAIN = env("EMAIL_SENDER_NAME", DOMAIN)
|
||||||
|
EMAIL_SENDER = f"{EMAIL_SENDER_NAME}@{EMAIL_SENDER_DOMAIN}"
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
@ -119,15 +121,43 @@ STREAMS = [
|
||||||
{"key": "books", "name": _("Books Timeline"), "shortname": _("Books")},
|
{"key": "books", "name": _("Books Timeline"), "shortname": _("Books")},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Search configuration
|
||||||
|
# total time in seconds that the instance will spend searching connectors
|
||||||
|
SEARCH_TIMEOUT = int(env("SEARCH_TIMEOUT", 15))
|
||||||
|
# timeout for a query to an individual connector
|
||||||
|
QUERY_TIMEOUT = int(env("QUERY_TIMEOUT", 5))
|
||||||
|
|
||||||
|
# Redis cache backend
|
||||||
|
if env("USE_DUMMY_CACHE", False):
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.cache.backends.dummy.DummyCache",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
|
"LOCATION": f"redis://:{REDIS_ACTIVITY_PASSWORD}@{REDIS_ACTIVITY_HOST}:{REDIS_ACTIVITY_PORT}/0",
|
||||||
|
"OPTIONS": {
|
||||||
|
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
||||||
|
SESSION_CACHE_ALIAS = "default"
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
||||||
"NAME": env("POSTGRES_DB", "fedireads"),
|
"NAME": env("POSTGRES_DB", "bookwyrm"),
|
||||||
"USER": env("POSTGRES_USER", "fedireads"),
|
"USER": env("POSTGRES_USER", "bookwyrm"),
|
||||||
"PASSWORD": env("POSTGRES_PASSWORD", "fedireads"),
|
"PASSWORD": env("POSTGRES_PASSWORD", "bookwyrm"),
|
||||||
"HOST": env("POSTGRES_HOST", ""),
|
"HOST": env("POSTGRES_HOST", ""),
|
||||||
"PORT": env("PGPORT", 5432),
|
"PORT": env("PGPORT", 5432),
|
||||||
},
|
},
|
||||||
|
@ -168,7 +198,8 @@ LANGUAGES = [
|
||||||
("gl-es", _("Galego (Galician)")),
|
("gl-es", _("Galego (Galician)")),
|
||||||
("fr-fr", _("Français (French)")),
|
("fr-fr", _("Français (French)")),
|
||||||
("lt-lt", _("Lietuvių (Lithuanian)")),
|
("lt-lt", _("Lietuvių (Lithuanian)")),
|
||||||
("pt-br", _("Português - Brasil (Brazilian Portuguese)")),
|
("pt-br", _("Português do Brasil (Brazilian Portuguese)")),
|
||||||
|
("pt-pt", _("Português Europeu (European Portuguese)")),
|
||||||
("zh-hans", _("简体中文 (Simplified Chinese)")),
|
("zh-hans", _("简体中文 (Simplified Chinese)")),
|
||||||
("zh-hant", _("繁體中文 (Traditional Chinese)")),
|
("zh-hant", _("繁體中文 (Traditional Chinese)")),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,6 +8,44 @@ body {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: auto;
|
||||||
|
overflow: visible;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
/* inherit font, color & alignment from ancestor */
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: inherit;
|
||||||
|
|
||||||
|
/* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
|
||||||
|
line-height: normal;
|
||||||
|
|
||||||
|
/* Corrects font smoothing for webkit */
|
||||||
|
-webkit-font-smoothing: inherit;
|
||||||
|
-moz-osx-font-smoothing: inherit;
|
||||||
|
|
||||||
|
/* Corrects inability to style clickable `input` types in iOS */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
|
||||||
|
/* Generalizes pointer cursor */
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button::-moz-focus-inner {
|
||||||
|
/* Remove excess padding and border in Firefox 4+ */
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better accessibility for keyboard users */
|
||||||
|
*:focus-visible {
|
||||||
|
outline-style: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
@ -29,10 +67,38 @@ body {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-card {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card > * {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* stylelint-disable no-descending-specificity */
|
||||||
|
.modal-card:focus {
|
||||||
|
outline-style: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card:focus:not(:focus-visible) {
|
||||||
|
outline-style: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card:focus-visible {
|
||||||
|
outline-style: auto;
|
||||||
|
}
|
||||||
|
/* stylelint-enable no-descending-specificity */
|
||||||
|
|
||||||
.modal-card.is-fullwidth {
|
.modal-card.is-fullwidth {
|
||||||
min-width: 75% !important;
|
min-width: 75% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 769px) {
|
||||||
|
.modal-card.is-thin {
|
||||||
|
width: 350px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.modal-card-body {
|
.modal-card-body {
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
}
|
}
|
||||||
|
@ -69,6 +135,18 @@ body {
|
||||||
border-bottom: 1px solid #ededed;
|
border-bottom: 1px solid #ededed;
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-flex-direction-row-mobile {
|
||||||
|
flex-direction: row !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-flex-direction-column-mobile {
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag.is-small {
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.is-transparent {
|
.button.is-transparent {
|
||||||
|
@ -93,7 +171,34 @@ body {
|
||||||
display: inline !important;
|
display: inline !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=file]::file-selector-button {
|
button .button-invisible-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 66%);
|
||||||
|
color: white;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover .button-invisible-overlay,
|
||||||
|
button:active .button-invisible-overlay,
|
||||||
|
button:focus-visible .button-invisible-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** File input styles
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
input[type="file"]::file-selector-button {
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
@ -114,15 +219,40 @@ input[type=file]::file-selector-button {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=file]::file-selector-button:hover {
|
input[type="file"]::file-selector-button:hover {
|
||||||
border-color: #b5b5b5;
|
border-color: #b5b5b5;
|
||||||
color: #363636;
|
color: #363636;
|
||||||
}
|
}
|
||||||
|
|
||||||
details .dropdown-menu {
|
/** General `details` element styles
|
||||||
display: block !important;
|
******************************************************************************/
|
||||||
|
|
||||||
|
details summary {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
details summary::marker {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.detail-pinned-button summary {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.detail-pinned-button form {
|
||||||
|
float: left;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dropdown w/ Details element
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
details.dropdown[open] summary.dropdown-trigger::before {
|
details.dropdown[open] summary.dropdown-trigger::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -132,19 +262,83 @@ details.dropdown[open] summary.dropdown-trigger::before {
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
summary::marker {
|
details.dropdown .dropdown-menu {
|
||||||
content: none;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-pinned-button summary {
|
details.dropdown .dropdown-menu button {
|
||||||
|
/* Fix weird Safari defaults */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.dropdown .dropdown-menu button:focus-visible,
|
||||||
|
details.dropdown .dropdown-menu a:focus-visible {
|
||||||
|
outline-style: auto;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
details.dropdown[open] summary.dropdown-trigger::before {
|
||||||
|
background-color: rgba(0, 0, 0, 50%);
|
||||||
|
z-index: 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
details .dropdown-menu {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
details .dropdown-menu > * {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Details panel
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
details.details-panel {
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 10%);
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open].details-panel,
|
||||||
|
details.details-panel:hover {
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
details.details-panel summary {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.details-panel summary .details-close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-pinned-button form {
|
details[open].details-panel summary .details-close {
|
||||||
float: left;
|
transform: rotate(0deg);
|
||||||
width: -webkit-fill-available;
|
}
|
||||||
margin-top: 1em;
|
|
||||||
|
@media only screen and (min-width: 769px) {
|
||||||
|
.details-panel .filters-field:not(:last-child) {
|
||||||
|
border-right: 1px solid rgba(0, 0, 0, 10%);
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shelving
|
/** Shelving
|
||||||
|
@ -153,7 +347,7 @@ summary::marker {
|
||||||
/** @todo Replace icons with SVG symbols.
|
/** @todo Replace icons with SVG symbols.
|
||||||
@see https://www.youtube.com/watch?v=9xXBYcWgCHA */
|
@see https://www.youtube.com/watch?v=9xXBYcWgCHA */
|
||||||
.shelf-option:disabled > *::after {
|
.shelf-option:disabled > *::after {
|
||||||
font-family: "icomoon"; /* stylelint-disable font-family-no-missing-generic-family-keyword */
|
font-family: icomoon; /* stylelint-disable font-family-no-missing-generic-family-keyword */
|
||||||
content: "\e919"; /* icon-check */
|
content: "\e919"; /* icon-check */
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
|
@ -161,14 +355,14 @@ summary::marker {
|
||||||
/** Toggles
|
/** Toggles
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
||||||
.toggle-button[aria-pressed=true],
|
.toggle-button[aria-pressed="true"],
|
||||||
.toggle-button[aria-pressed=true]:hover {
|
.toggle-button[aria-pressed="true"]:hover {
|
||||||
background-color: hsl(171, 100%, 41%);
|
background-color: hsl(171deg, 100%, 41%);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-active[aria-pressed=true],
|
.hide-active[aria-pressed="true"],
|
||||||
.hide-inactive[aria-pressed=false] {
|
.hide-inactive[aria-pressed="false"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,36 +419,36 @@ summary::marker {
|
||||||
|
|
||||||
/* All stars are visually filled by default. */
|
/* All stars are visually filled by default. */
|
||||||
.form-rate-stars .icon::before {
|
.form-rate-stars .icon::before {
|
||||||
content: '\e9d9'; /* icon-star-full */
|
content: "\e9d9"; /* icon-star-full */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icons directly following half star inputs are marked as half */
|
/* Icons directly following half star inputs are marked as half */
|
||||||
.form-rate-stars input.half:checked ~ .icon::before {
|
.form-rate-stars input.half:checked ~ .icon::before {
|
||||||
content: '\e9d8'; /* icon-star-half */
|
content: "\e9d8"; /* icon-star-half */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* stylelint-disable no-descending-specificity */
|
/* stylelint-disable no-descending-specificity */
|
||||||
.form-rate-stars input.half:checked + input + .icon:hover::before {
|
.form-rate-stars input.half:checked + input + .icon:hover::before {
|
||||||
content: '\e9d8' !important; /* icon-star-half */
|
content: "\e9d8" !important; /* icon-star-half */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icons directly following half check inputs that follow the checked input are emptied. */
|
/* Icons directly following half check inputs that follow the checked input are emptied. */
|
||||||
.form-rate-stars input.half:checked + input + .icon ~ .icon::before {
|
.form-rate-stars input.half:checked + input + .icon ~ .icon::before {
|
||||||
content: '\e9d7'; /* icon-star-empty */
|
content: "\e9d7"; /* icon-star-empty */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icons directly following inputs that follow the checked input are emptied. */
|
/* Icons directly following inputs that follow the checked input are emptied. */
|
||||||
.form-rate-stars input:checked ~ input + .icon::before {
|
.form-rate-stars input:checked ~ input + .icon::before {
|
||||||
content: '\e9d7'; /* icon-star-empty */
|
content: "\e9d7"; /* icon-star-empty */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* When a label is hovered, repeat the fill-all-then-empty-following pattern. */
|
/* When a label is hovered, repeat the fill-all-then-empty-following pattern. */
|
||||||
.form-rate-stars:hover .icon.icon::before {
|
.form-rate-stars:hover .icon.icon::before {
|
||||||
content: '\e9d9' !important; /* icon-star-full */
|
content: "\e9d9" !important; /* icon-star-full */
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-rate-stars .icon:hover ~ .icon::before {
|
.form-rate-stars .icon:hover ~ .icon::before {
|
||||||
content: '\e9d7' !important; /* icon-star-empty */
|
content: "\e9d7" !important; /* icon-star-empty */
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Book covers
|
/** Book covers
|
||||||
|
@ -322,6 +516,8 @@ summary::marker {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
white-space: initial;
|
white-space: initial;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -360,7 +556,7 @@ summary::marker {
|
||||||
|
|
||||||
.quote > blockquote::before,
|
.quote > blockquote::before,
|
||||||
.quote > blockquote::after {
|
.quote > blockquote::after {
|
||||||
font-family: 'icomoon';
|
font-family: icomoon;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -555,6 +751,74 @@ ol.ordered-list li::before {
|
||||||
padding: 0 0.75em;
|
padding: 0 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Breadcrumbs
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
.books-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: end;
|
||||||
|
justify-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.books-grid > .is-big {
|
||||||
|
grid-column: span 2;
|
||||||
|
grid-row: span 2;
|
||||||
|
justify-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.books-grid .book-cover {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.books-grid .book-title {
|
||||||
|
--height-basis: 1.35rem;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
line-height: var(--height-basis);
|
||||||
|
min-height: calc(2 * var(--height-basis));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 769px) {
|
||||||
|
.books-grid {
|
||||||
|
gap: 1.5rem;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(8em, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
.horizontal-copy {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-copy textarea {
|
||||||
|
min-width: initial;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-copy button {
|
||||||
|
align-self: stretch;
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-copy {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-copy button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dimensions
|
/* Dimensions
|
||||||
* @todo These could be in rem.
|
* @todo These could be in rem.
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
@ -937,3 +1201,93 @@ ol.ordered-list li::before {
|
||||||
margin-bottom: 0.75rem !important;
|
margin-bottom: 0.75rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Gaps (for Flexbox and Grid)
|
||||||
|
*
|
||||||
|
* Those are supplementary rules to Bulma’s. They follow the same conventions.
|
||||||
|
* Add those you’ll need.
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
.is-gap-0 {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-gap-1 {
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-gap-2 {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-gap-3 {
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-gap-4 {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-gap-5 {
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-gap-6 {
|
||||||
|
gap: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-row-gap-0 {
|
||||||
|
row-gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-row-gap-1 {
|
||||||
|
row-gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-row-gap-2 {
|
||||||
|
row-gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-row-gap-3 {
|
||||||
|
row-gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-row-gap-4 {
|
||||||
|
row-gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-row-gap-5 {
|
||||||
|
row-gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-row-gap-6 {
|
||||||
|
row-gap: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-column-gap-0 {
|
||||||
|
column-gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-column-gap-1 {
|
||||||
|
column-gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-column-gap-2 {
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-column-gap-3 {
|
||||||
|
column-gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-column-gap-4 {
|
||||||
|
column-gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-column-gap-5 {
|
||||||
|
column-gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-column-gap-6 {
|
||||||
|
column-gap: 3rem;
|
||||||
|
}
|
||||||
|
|
95
bookwyrm/static/css/fonts/dm_serif_display/OFL.txt
Normal file
95
bookwyrm/static/css/fonts/dm_serif_display/OFL.txt
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
Copyright 2014-2018 Adobe (http://www.adobe.com/), with Reserved Font Name
|
||||||
|
'Source'. All Rights Reserved. Source is a trademark of Adobe in the United
|
||||||
|
States and/or other countries. Copyright 2019 Google LLC.
|
||||||
|
|
||||||
|
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.
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,30 @@
|
||||||
|
# Font Squirrel Font-face Generator Configuration File
|
||||||
|
# Upload this file to the generator to recreate the settings
|
||||||
|
# you used to create these fonts.
|
||||||
|
|
||||||
|
{
|
||||||
|
"mode": "optimal",
|
||||||
|
"formats":
|
||||||
|
[
|
||||||
|
"woff",
|
||||||
|
"woff2"
|
||||||
|
],
|
||||||
|
"tt_instructor": "default",
|
||||||
|
"fix_gasp": "xy",
|
||||||
|
"fix_vertical_metrics": "Y",
|
||||||
|
"metrics_ascent": "",
|
||||||
|
"metrics_descent": "",
|
||||||
|
"metrics_linegap": "",
|
||||||
|
"add_spaces": "Y",
|
||||||
|
"add_hyphens": "Y",
|
||||||
|
"fallback": "none",
|
||||||
|
"fallback_custom": "100",
|
||||||
|
"options_subset": "basic",
|
||||||
|
"subset_custom": "",
|
||||||
|
"subset_custom_range": "",
|
||||||
|
"subset_ot_features_list": "",
|
||||||
|
"css_stylesheet": "stylesheet.css",
|
||||||
|
"filename_suffix": "-webfont",
|
||||||
|
"emsquare": "2048",
|
||||||
|
"spacing_adjustment": "0"
|
||||||
|
}
|
19
bookwyrm/static/css/vendor/dm_serif_display.css
vendored
Normal file
19
bookwyrm/static/css/vendor/dm_serif_display.css
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'dm_serif_display';
|
||||||
|
src: url('../fonts/dm_serif_display/dmserifdisplay-italic-webfont.woff2') format('woff2'),
|
||||||
|
url('../fonts/dm_serif_display/dmserifdisplay-italic-webfont.woff') format('woff');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'dm_serif_display';
|
||||||
|
src: url('../fonts/dm_serif_display/dmserifdisplay-regular-webfont.woff2') format('woff2'),
|
||||||
|
url('../fonts/dm_serif_display/dmserifdisplay-regular-webfont.woff') format('woff');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-serif {
|
||||||
|
font-family: 'dm_serif_display', Georgia, serif;
|
||||||
|
}
|
4
bookwyrm/static/css/vendor/icons.css
vendored
4
bookwyrm/static/css/vendor/icons.css
vendored
|
@ -25,6 +25,10 @@
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon.is-small {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
.icon-book:before {
|
.icon-book:before {
|
||||||
content: "\e901";
|
content: "\e901";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
/* exported BlockHref */
|
|
||||||
|
|
||||||
let BlockHref = new class {
|
|
||||||
constructor() {
|
|
||||||
document.querySelectorAll('[data-href]')
|
|
||||||
.forEach(t => t.addEventListener('click', this.followLink.bind(this)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Follow a fake link
|
|
||||||
*
|
|
||||||
* @param {Event} event
|
|
||||||
* @return {undefined}
|
|
||||||
*/
|
|
||||||
followLink(event) {
|
|
||||||
const url = event.currentTarget.dataset.href;
|
|
||||||
|
|
||||||
window.location.href = url;
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* exported BookWyrm */
|
/* exported BookWyrm */
|
||||||
/* globals TabGroup */
|
/* globals TabGroup */
|
||||||
|
|
||||||
let BookWyrm = new class {
|
let BookWyrm = new (class {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.MAX_FILE_SIZE_BYTES = 10 * 1000000;
|
this.MAX_FILE_SIZE_BYTES = 10 * 1000000;
|
||||||
this.initOnDOMLoaded();
|
this.initOnDOMLoaded();
|
||||||
|
@ -10,48 +10,43 @@ let BookWyrm = new class {
|
||||||
}
|
}
|
||||||
|
|
||||||
initEventListeners() {
|
initEventListeners() {
|
||||||
document.querySelectorAll('[data-controls]')
|
document
|
||||||
.forEach(button => button.addEventListener(
|
.querySelectorAll("[data-controls]")
|
||||||
'click',
|
.forEach((button) => button.addEventListener("click", this.toggleAction.bind(this)));
|
||||||
this.toggleAction.bind(this))
|
|
||||||
);
|
|
||||||
|
|
||||||
document.querySelectorAll('.interaction')
|
document
|
||||||
.forEach(button => button.addEventListener(
|
.querySelectorAll(".interaction")
|
||||||
'submit',
|
.forEach((button) => button.addEventListener("submit", this.interact.bind(this)));
|
||||||
this.interact.bind(this))
|
|
||||||
);
|
|
||||||
|
|
||||||
document.querySelectorAll('.hidden-form input')
|
document
|
||||||
.forEach(button => button.addEventListener(
|
.querySelectorAll(".hidden-form input")
|
||||||
'change',
|
.forEach((button) => button.addEventListener("change", this.revealForm.bind(this)));
|
||||||
this.revealForm.bind(this))
|
|
||||||
);
|
|
||||||
|
|
||||||
document.querySelectorAll('[data-hides]')
|
document
|
||||||
.forEach(button => button.addEventListener(
|
.querySelectorAll("[data-hides]")
|
||||||
'change',
|
.forEach((button) => button.addEventListener("change", this.hideForm.bind(this)));
|
||||||
this.hideForm.bind(this))
|
|
||||||
);
|
|
||||||
|
|
||||||
document.querySelectorAll('[data-back]')
|
document
|
||||||
.forEach(button => button.addEventListener(
|
.querySelectorAll("[data-back]")
|
||||||
'click',
|
.forEach((button) => button.addEventListener("click", this.back));
|
||||||
this.back)
|
|
||||||
);
|
|
||||||
|
|
||||||
document.querySelectorAll('input[type="file"]')
|
document
|
||||||
.forEach(node => node.addEventListener(
|
.querySelectorAll('input[type="file"]')
|
||||||
'change',
|
.forEach((node) => node.addEventListener("change", this.disableIfTooLarge.bind(this)));
|
||||||
this.disableIfTooLarge.bind(this)
|
|
||||||
));
|
document
|
||||||
|
.querySelectorAll("button[data-modal-open]")
|
||||||
document.querySelectorAll('[data-duplicate]')
|
.forEach((node) => node.addEventListener("click", this.handleModalButton.bind(this)));
|
||||||
.forEach(node => node.addEventListener(
|
|
||||||
'click',
|
document
|
||||||
this.duplicateInput.bind(this)
|
.querySelectorAll("[data-duplicate]")
|
||||||
|
.forEach((node) => node.addEventListener("click", this.duplicateInput.bind(this)));
|
||||||
))
|
|
||||||
|
document
|
||||||
|
.querySelectorAll("details.dropdown")
|
||||||
|
.forEach((node) =>
|
||||||
|
node.addEventListener("toggle", this.handleDetailsDropdown.bind(this))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,15 +55,15 @@ let BookWyrm = new class {
|
||||||
initOnDOMLoaded() {
|
initOnDOMLoaded() {
|
||||||
const bookwyrm = this;
|
const bookwyrm = this;
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', function() {
|
window.addEventListener("DOMContentLoaded", function () {
|
||||||
document.querySelectorAll('.tab-group')
|
document.querySelectorAll(".tab-group").forEach((tabs) => new TabGroup(tabs));
|
||||||
.forEach(tabs => new TabGroup(tabs));
|
document
|
||||||
document.querySelectorAll('input[type="file"]').forEach(
|
.querySelectorAll('input[type="file"]')
|
||||||
bookwyrm.disableIfTooLarge.bind(bookwyrm)
|
.forEach(bookwyrm.disableIfTooLarge.bind(bookwyrm));
|
||||||
);
|
document.querySelectorAll("[data-copytext]").forEach(bookwyrm.copyText.bind(bookwyrm));
|
||||||
document.querySelectorAll('[data-copytext]').forEach(
|
document
|
||||||
bookwyrm.copyText.bind(bookwyrm)
|
.querySelectorAll(".modal.is-active")
|
||||||
);
|
.forEach(bookwyrm.handleActiveModal.bind(bookwyrm));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,8 +72,7 @@ let BookWyrm = new class {
|
||||||
*/
|
*/
|
||||||
initReccuringTasks() {
|
initReccuringTasks() {
|
||||||
// Polling
|
// Polling
|
||||||
document.querySelectorAll('[data-poll]')
|
document.querySelectorAll("[data-poll]").forEach((liveArea) => this.polling(liveArea));
|
||||||
.forEach(liveArea => this.polling(liveArea));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -104,15 +98,19 @@ let BookWyrm = new class {
|
||||||
const bookwyrm = this;
|
const bookwyrm = this;
|
||||||
|
|
||||||
delay = delay || 10000;
|
delay = delay || 10000;
|
||||||
delay += (Math.random() * 1000);
|
delay += Math.random() * 1000;
|
||||||
|
|
||||||
setTimeout(function() {
|
setTimeout(
|
||||||
fetch('/api/updates/' + counter.dataset.poll)
|
function () {
|
||||||
.then(response => response.json())
|
fetch("/api/updates/" + counter.dataset.poll)
|
||||||
.then(data => bookwyrm.updateCountElement(counter, data));
|
.then((response) => response.json())
|
||||||
|
.then((data) => bookwyrm.updateCountElement(counter, data));
|
||||||
|
|
||||||
bookwyrm.polling(counter, delay * 1.25);
|
bookwyrm.polling(counter, delay * 1.25);
|
||||||
}, delay, counter);
|
},
|
||||||
|
delay,
|
||||||
|
counter
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -127,60 +125,56 @@ let BookWyrm = new class {
|
||||||
const count_by_type = data.count_by_type;
|
const count_by_type = data.count_by_type;
|
||||||
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');
|
const allowedStatusTypesEl = document.getElementById("unread-notifications-wrapper");
|
||||||
|
|
||||||
// If we're on the right counter element
|
// If we're on the right counter element
|
||||||
if (counter.closest('[data-poll-wrapper]').contains(allowedStatusTypesEl)) {
|
if (counter.closest("[data-poll-wrapper]").contains(allowedStatusTypesEl)) {
|
||||||
const allowedStatusTypes = JSON.parse(allowedStatusTypesEl.textContent);
|
const allowedStatusTypes = JSON.parse(allowedStatusTypesEl.textContent);
|
||||||
|
|
||||||
// For keys in common between allowedStatusTypes and count_by_type
|
// For keys in common between allowedStatusTypes and count_by_type
|
||||||
// This concerns 'review', 'quotation', 'comment'
|
// This concerns 'review', 'quotation', 'comment'
|
||||||
count = allowedStatusTypes.reduce(function(prev, currentKey) {
|
count = allowedStatusTypes.reduce(function (prev, currentKey) {
|
||||||
const currentValue = count_by_type[currentKey] | 0;
|
const currentValue = count_by_type[currentKey] | 0;
|
||||||
|
|
||||||
return prev + currentValue;
|
return prev + currentValue;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// Add all the "other" in count_by_type if 'everything' is allowed
|
// Add all the "other" in count_by_type if 'everything' is allowed
|
||||||
if (allowedStatusTypes.includes('everything')) {
|
if (allowedStatusTypes.includes("everything")) {
|
||||||
// Clone count_by_type with 0 for reviews/quotations/comments
|
// Clone count_by_type with 0 for reviews/quotations/comments
|
||||||
const count_by_everything_else = Object.assign(
|
const count_by_everything_else = Object.assign({}, count_by_type, {
|
||||||
{},
|
review: 0,
|
||||||
count_by_type,
|
quotation: 0,
|
||||||
{review: 0, quotation: 0, comment: 0}
|
comment: 0,
|
||||||
);
|
});
|
||||||
|
|
||||||
count = Object.keys(count_by_everything_else).reduce(
|
count = Object.keys(count_by_everything_else).reduce(function (prev, currentKey) {
|
||||||
function(prev, currentKey) {
|
const currentValue = count_by_everything_else[currentKey] | 0;
|
||||||
const currentValue =
|
|
||||||
count_by_everything_else[currentKey] | 0
|
|
||||||
|
|
||||||
return prev + currentValue;
|
return prev + currentValue;
|
||||||
},
|
}, count);
|
||||||
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);
|
||||||
counter.innerText = count;
|
counter.innerText = count;
|
||||||
this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-danger', hasMentions);
|
this.addRemoveClass(counter.closest("[data-poll-wrapper]"), "is-danger", hasMentions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show form.
|
* Show form.
|
||||||
*
|
*
|
||||||
* @param {Event} event
|
* @param {Event} event
|
||||||
* @return {undefined}
|
* @return {undefined}
|
||||||
*/
|
*/
|
||||||
revealForm(event) {
|
revealForm(event) {
|
||||||
let trigger = event.currentTarget;
|
let trigger = event.currentTarget;
|
||||||
let hidden = trigger.closest('.hidden-form').querySelectorAll('.is-hidden')[0];
|
let hidden = trigger.closest(".hidden-form").querySelectorAll(".is-hidden")[0];
|
||||||
|
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
this.addRemoveClass(hidden, 'is-hidden', !hidden);
|
this.addRemoveClass(hidden, "is-hidden", !hidden);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,10 +186,10 @@ let BookWyrm = new class {
|
||||||
*/
|
*/
|
||||||
hideForm(event) {
|
hideForm(event) {
|
||||||
let trigger = event.currentTarget;
|
let trigger = event.currentTarget;
|
||||||
let targetId = trigger.dataset.hides
|
let targetId = trigger.dataset.hides;
|
||||||
let visible = document.getElementById(targetId)
|
let visible = document.getElementById(targetId);
|
||||||
|
|
||||||
this.addRemoveClass(visible, 'is-hidden', true);
|
this.addRemoveClass(visible, "is-hidden", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -210,31 +204,34 @@ let BookWyrm = new class {
|
||||||
if (!trigger.dataset.allowDefault || event.currentTarget == event.target) {
|
if (!trigger.dataset.allowDefault || event.currentTarget == event.target) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
let pressed = trigger.getAttribute('aria-pressed') === 'false';
|
let pressed = trigger.getAttribute("aria-pressed") === "false";
|
||||||
let targetId = trigger.dataset.controls;
|
let targetId = trigger.dataset.controls;
|
||||||
|
|
||||||
// Toggle pressed status on all triggers controlling the same target.
|
// Toggle pressed status on all triggers controlling the same target.
|
||||||
document.querySelectorAll('[data-controls="' + targetId + '"]')
|
document
|
||||||
.forEach(otherTrigger => otherTrigger.setAttribute(
|
.querySelectorAll('[data-controls="' + targetId + '"]')
|
||||||
'aria-pressed',
|
.forEach((otherTrigger) =>
|
||||||
otherTrigger.getAttribute('aria-pressed') === 'false'
|
otherTrigger.setAttribute(
|
||||||
));
|
"aria-pressed",
|
||||||
|
otherTrigger.getAttribute("aria-pressed") === "false"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// @todo Find a better way to handle the exception.
|
// @todo Find a better way to handle the exception.
|
||||||
if (targetId && ! trigger.classList.contains('pulldown-menu')) {
|
if (targetId && !trigger.classList.contains("pulldown-menu")) {
|
||||||
let target = document.getElementById(targetId);
|
let target = document.getElementById(targetId);
|
||||||
|
|
||||||
this.addRemoveClass(target, 'is-hidden', !pressed);
|
this.addRemoveClass(target, "is-hidden", !pressed);
|
||||||
this.addRemoveClass(target, 'is-active', pressed);
|
this.addRemoveClass(target, "is-active", pressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide pulldown-menus.
|
// Show/hide pulldown-menus.
|
||||||
if (trigger.classList.contains('pulldown-menu')) {
|
if (trigger.classList.contains("pulldown-menu")) {
|
||||||
this.toggleMenu(trigger, targetId);
|
this.toggleMenu(trigger, targetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide container.
|
// Show/hide container.
|
||||||
let container = document.getElementById('hide_' + targetId);
|
let container = document.getElementById("hide_" + targetId);
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
this.toggleContainer(container, pressed);
|
this.toggleContainer(container, pressed);
|
||||||
|
@ -271,14 +268,14 @@ let BookWyrm = new class {
|
||||||
* @return {undefined}
|
* @return {undefined}
|
||||||
*/
|
*/
|
||||||
toggleMenu(trigger, targetId) {
|
toggleMenu(trigger, targetId) {
|
||||||
let expanded = trigger.getAttribute('aria-expanded') == 'false';
|
let expanded = trigger.getAttribute("aria-expanded") == "false";
|
||||||
|
|
||||||
trigger.setAttribute('aria-expanded', expanded);
|
trigger.setAttribute("aria-expanded", expanded);
|
||||||
|
|
||||||
if (targetId) {
|
if (targetId) {
|
||||||
let target = document.getElementById(targetId);
|
let target = document.getElementById(targetId);
|
||||||
|
|
||||||
this.addRemoveClass(target, 'is-active', expanded);
|
this.addRemoveClass(target, "is-active", expanded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,7 +287,7 @@ let BookWyrm = new class {
|
||||||
* @return {undefined}
|
* @return {undefined}
|
||||||
*/
|
*/
|
||||||
toggleContainer(container, pressed) {
|
toggleContainer(container, pressed) {
|
||||||
this.addRemoveClass(container, 'is-hidden', pressed);
|
this.addRemoveClass(container, "is-hidden", pressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -327,7 +324,7 @@ let BookWyrm = new class {
|
||||||
|
|
||||||
node.focus();
|
node.focus();
|
||||||
|
|
||||||
setTimeout(function() {
|
setTimeout(function () {
|
||||||
node.selectionStart = node.selectionEnd = 10000;
|
node.selectionStart = node.selectionEnd = 10000;
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
@ -347,15 +344,17 @@ let BookWyrm = new class {
|
||||||
const relatedforms = document.querySelectorAll(`.${form.dataset.id}`);
|
const relatedforms = document.querySelectorAll(`.${form.dataset.id}`);
|
||||||
|
|
||||||
// Toggle class on all related forms.
|
// Toggle class on all related forms.
|
||||||
relatedforms.forEach(relatedForm => bookwyrm.addRemoveClass(
|
relatedforms.forEach((relatedForm) =>
|
||||||
relatedForm,
|
bookwyrm.addRemoveClass(
|
||||||
'is-hidden',
|
relatedForm,
|
||||||
relatedForm.className.indexOf('is-hidden') == -1
|
"is-hidden",
|
||||||
));
|
relatedForm.className.indexOf("is-hidden") == -1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
this.ajaxPost(form).catch(error => {
|
this.ajaxPost(form).catch((error) => {
|
||||||
// @todo Display a notification in the UI instead.
|
// @todo Display a notification in the UI instead.
|
||||||
console.warn('Request failed:', error);
|
console.warn("Request failed:", error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,11 +366,11 @@ let BookWyrm = new class {
|
||||||
*/
|
*/
|
||||||
ajaxPost(form) {
|
ajaxPost(form) {
|
||||||
return fetch(form.action, {
|
return fetch(form.action, {
|
||||||
method : "POST",
|
method: "POST",
|
||||||
body: new FormData(form),
|
body: new FormData(form),
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,21 +395,112 @@ let BookWyrm = new class {
|
||||||
const element = eventOrElement.currentTarget || eventOrElement;
|
const element = eventOrElement.currentTarget || eventOrElement;
|
||||||
|
|
||||||
const submits = element.form.querySelectorAll('[type="submit"]');
|
const submits = element.form.querySelectorAll('[type="submit"]');
|
||||||
const warns = element.parentElement.querySelectorAll('.file-too-big');
|
const warns = element.parentElement.querySelectorAll(".file-too-big");
|
||||||
const isTooBig = element.files &&
|
const isTooBig =
|
||||||
element.files[0] &&
|
element.files && element.files[0] && element.files[0].size > MAX_FILE_SIZE_BYTES;
|
||||||
element.files[0].size > MAX_FILE_SIZE_BYTES;
|
|
||||||
|
|
||||||
if (isTooBig) {
|
if (isTooBig) {
|
||||||
submits.forEach(submitter => submitter.disabled = true);
|
submits.forEach((submitter) => (submitter.disabled = true));
|
||||||
warns.forEach(
|
warns.forEach((sib) => addRemoveClass(sib, "is-hidden", false));
|
||||||
sib => addRemoveClass(sib, 'is-hidden', false)
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
submits.forEach(submitter => submitter.disabled = false);
|
submits.forEach((submitter) => (submitter.disabled = false));
|
||||||
warns.forEach(
|
warns.forEach((sib) => addRemoveClass(sib, "is-hidden", true));
|
||||||
sib => addRemoveClass(sib, 'is-hidden', true)
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the modal component with a button trigger.
|
||||||
|
*
|
||||||
|
* @param {Event} event - Event fired by an element
|
||||||
|
* with the `data-modal-open` attribute
|
||||||
|
* pointing to a modal by its id.
|
||||||
|
* @return {undefined}
|
||||||
|
*
|
||||||
|
* See https://github.com/bookwyrm-social/bookwyrm/pull/1633
|
||||||
|
* for information about using the modal.
|
||||||
|
*/
|
||||||
|
handleModalButton(event) {
|
||||||
|
const { handleFocusTrap } = this;
|
||||||
|
const modalButton = event.currentTarget;
|
||||||
|
const targetModalId = modalButton.dataset.modalOpen;
|
||||||
|
const htmlElement = document.querySelector("html");
|
||||||
|
const modal = document.getElementById(targetModalId);
|
||||||
|
|
||||||
|
if (!modal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function handleModalOpen(modalElement) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
htmlElement.classList.add("is-clipped");
|
||||||
|
modalElement.classList.add("is-active");
|
||||||
|
modalElement.getElementsByClassName("modal-card")[0].focus();
|
||||||
|
|
||||||
|
const closeButtons = modalElement.querySelectorAll("[data-modal-close]");
|
||||||
|
|
||||||
|
closeButtons.forEach((button) => {
|
||||||
|
button.addEventListener("click", function () {
|
||||||
|
handleModalClose(modalElement);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", function (event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
handleModalClose(modalElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
modalElement.addEventListener("keydown", handleFocusTrap);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModalClose(modalElement) {
|
||||||
|
modalElement.removeEventListener("keydown", handleFocusTrap);
|
||||||
|
htmlElement.classList.remove("is-clipped");
|
||||||
|
modalElement.classList.remove("is-active");
|
||||||
|
modalButton.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open modal
|
||||||
|
handleModalOpen(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the modal component when opened at page load.
|
||||||
|
*
|
||||||
|
* @param {Element} modalElement - Active modal element
|
||||||
|
* @return {undefined}
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
handleActiveModal(modalElement) {
|
||||||
|
if (!modalElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { handleFocusTrap } = this;
|
||||||
|
|
||||||
|
modalElement.getElementsByClassName("modal-card")[0].focus();
|
||||||
|
|
||||||
|
const closeButtons = modalElement.querySelectorAll("[data-modal-close]");
|
||||||
|
|
||||||
|
closeButtons.forEach((button) => {
|
||||||
|
button.addEventListener("click", function () {
|
||||||
|
handleModalClose(modalElement);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", function (event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
handleModalClose(modalElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
modalElement.addEventListener("keydown", handleFocusTrap);
|
||||||
|
|
||||||
|
function handleModalClose(modalElement) {
|
||||||
|
modalElement.removeEventListener("keydown", handleFocusTrap);
|
||||||
|
history.back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -422,31 +512,27 @@ let BookWyrm = new class {
|
||||||
* @return {undefined}
|
* @return {undefined}
|
||||||
*/
|
*/
|
||||||
displayPopUp(url, windowName) {
|
displayPopUp(url, windowName) {
|
||||||
window.open(
|
window.open(url, windowName, "left=100,top=100,width=430,height=600");
|
||||||
url,
|
|
||||||
windowName,
|
|
||||||
"left=100,top=100,width=430,height=600"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
let input = orig.cloneNode();
|
let input = orig.cloneNode();
|
||||||
|
|
||||||
input.id += ("-" + (new_count))
|
input.id += "-" + new_count;
|
||||||
input.value = ""
|
input.value = "";
|
||||||
|
|
||||||
let label = parent.querySelector("label").cloneNode();
|
let label = parent.querySelector("label").cloneNode();
|
||||||
|
|
||||||
label.setAttribute("for", input.id)
|
label.setAttribute("for", input.id);
|
||||||
|
|
||||||
parent.appendChild(label)
|
parent.appendChild(label);
|
||||||
parent.appendChild(input)
|
parent.appendChild(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -461,25 +547,115 @@ let BookWyrm = new class {
|
||||||
copyText(textareaEl) {
|
copyText(textareaEl) {
|
||||||
const text = textareaEl.textContent;
|
const text = textareaEl.textContent;
|
||||||
|
|
||||||
const copyButtonEl = document.createElement('button');
|
const copyButtonEl = document.createElement("button");
|
||||||
|
|
||||||
copyButtonEl.textContent = textareaEl.dataset.copytextLabel;
|
copyButtonEl.textContent = textareaEl.dataset.copytextLabel;
|
||||||
copyButtonEl.classList.add(
|
copyButtonEl.classList.add("button", "is-small", "is-primary", "is-light");
|
||||||
"mt-2",
|
copyButtonEl.addEventListener("click", () => {
|
||||||
"button",
|
navigator.clipboard.writeText(text).then(function () {
|
||||||
"is-small",
|
textareaEl.classList.add("is-success");
|
||||||
"is-fullwidth",
|
copyButtonEl.classList.replace("is-primary", "is-success");
|
||||||
"is-primary",
|
|
||||||
"is-light"
|
|
||||||
);
|
|
||||||
copyButtonEl.addEventListener('click', () => {
|
|
||||||
navigator.clipboard.writeText(text).then(function() {
|
|
||||||
textareaEl.classList.add('is-success');
|
|
||||||
copyButtonEl.classList.replace('is-primary', 'is-success');
|
|
||||||
copyButtonEl.textContent = textareaEl.dataset.copytextSuccess;
|
copyButtonEl.textContent = textareaEl.dataset.copytextSuccess;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
textareaEl.parentNode.appendChild(copyButtonEl)
|
textareaEl.parentNode.appendChild(copyButtonEl);
|
||||||
}
|
}
|
||||||
}();
|
|
||||||
|
/**
|
||||||
|
* Handle the details dropdown component.
|
||||||
|
*
|
||||||
|
* @param {Event} event - Event fired by a `details` element
|
||||||
|
* with the `dropdown` class name, on toggle.
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
handleDetailsDropdown(event) {
|
||||||
|
const detailsElement = event.target;
|
||||||
|
const summaryElement = detailsElement.querySelector("summary");
|
||||||
|
const menuElement = detailsElement.querySelector(".dropdown-menu");
|
||||||
|
const htmlElement = document.querySelector("html");
|
||||||
|
|
||||||
|
if (detailsElement.open) {
|
||||||
|
// Focus first menu element
|
||||||
|
menuElement
|
||||||
|
.querySelectorAll("a[href]:not([disabled]), button:not([disabled])")[0]
|
||||||
|
.focus();
|
||||||
|
|
||||||
|
// Enable focus trap
|
||||||
|
menuElement.addEventListener("keydown", this.handleFocusTrap);
|
||||||
|
|
||||||
|
// Close on Esc
|
||||||
|
detailsElement.addEventListener("keydown", handleEscKey);
|
||||||
|
|
||||||
|
// Clip page if Mobile
|
||||||
|
if (this.isMobile()) {
|
||||||
|
htmlElement.classList.add("is-clipped");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
summaryElement.focus();
|
||||||
|
|
||||||
|
// Disable focus trap
|
||||||
|
menuElement.removeEventListener("keydown", this.handleFocusTrap);
|
||||||
|
|
||||||
|
// Unclip page
|
||||||
|
if (this.isMobile()) {
|
||||||
|
htmlElement.classList.remove("is-clipped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEscKey(event) {
|
||||||
|
if (event.key !== "Escape") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryElement.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if windows matches mobile media query.
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
isMobile() {
|
||||||
|
return window.matchMedia("(max-width: 768px)").matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focus trap handler
|
||||||
|
*
|
||||||
|
* @param {Event} event - Keydown event.
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
handleFocusTrap(event) {
|
||||||
|
if (event.key !== "Tab") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusableEls = event.currentTarget.querySelectorAll(
|
||||||
|
[
|
||||||
|
"a[href]:not([disabled])",
|
||||||
|
"button:not([disabled])",
|
||||||
|
"textarea:not([disabled])",
|
||||||
|
'input:not([type="hidden"]):not([disabled])',
|
||||||
|
"select:not([disabled])",
|
||||||
|
"details:not([disabled])",
|
||||||
|
'[tabindex]:not([tabindex="-1"]):not([disabled])',
|
||||||
|
].join(",")
|
||||||
|
);
|
||||||
|
const firstFocusableEl = focusableEls[0];
|
||||||
|
const lastFocusableEl = focusableEls[focusableEls.length - 1];
|
||||||
|
|
||||||
|
if (event.shiftKey) {
|
||||||
|
/* Shift + tab */ if (document.activeElement === firstFocusableEl) {
|
||||||
|
lastFocusableEl.focus();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
} /* Tab */ else {
|
||||||
|
if (document.activeElement === lastFocusableEl) {
|
||||||
|
firstFocusableEl.focus();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle all descendant checkboxes of a target.
|
|
||||||
*
|
|
||||||
* Use `data-target="ID_OF_TARGET"` on the node on which the event is listened
|
|
||||||
* to (checkbox, button, link…), where_ID_OF_TARGET_ should be the ID of an
|
|
||||||
* ancestor for the checkboxes.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* <input
|
|
||||||
* type="checkbox"
|
|
||||||
* data-action="toggle-all"
|
|
||||||
* data-target="failed-imports"
|
|
||||||
* >
|
|
||||||
* @param {Event} event
|
|
||||||
* @return {undefined}
|
|
||||||
*/
|
|
||||||
function toggleAllCheckboxes(event) {
|
|
||||||
const mainCheckbox = event.target;
|
|
||||||
|
|
||||||
document
|
|
||||||
.querySelectorAll(`#${mainCheckbox.dataset.target} [type="checkbox"]`)
|
|
||||||
.forEach(checkbox => checkbox.checked = mainCheckbox.checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
document
|
|
||||||
.querySelectorAll('[data-action="toggle-all"]')
|
|
||||||
.forEach(input => {
|
|
||||||
input.addEventListener('change', toggleAllCheckboxes);
|
|
||||||
});
|
|
||||||
})();
|
|
|
@ -1,13 +1,13 @@
|
||||||
/* exported LocalStorageTools */
|
/* exported LocalStorageTools */
|
||||||
/* globals BookWyrm */
|
/* globals BookWyrm */
|
||||||
|
|
||||||
let LocalStorageTools = new class {
|
let LocalStorageTools = new (class {
|
||||||
constructor() {
|
constructor() {
|
||||||
document.querySelectorAll('[data-hide]')
|
document.querySelectorAll("[data-hide]").forEach((t) => this.setDisplay(t));
|
||||||
.forEach(t => this.setDisplay(t));
|
|
||||||
|
|
||||||
document.querySelectorAll('.set-display')
|
document
|
||||||
.forEach(t => t.addEventListener('click', this.updateDisplay.bind(this)));
|
.querySelectorAll(".set-display")
|
||||||
|
.forEach((t) => t.addEventListener("click", this.updateDisplay.bind(this)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,8 +23,9 @@ let LocalStorageTools = new class {
|
||||||
|
|
||||||
window.localStorage.setItem(key, value);
|
window.localStorage.setItem(key, value);
|
||||||
|
|
||||||
document.querySelectorAll('[data-hide="' + key + '"]')
|
document
|
||||||
.forEach(node => this.setDisplay(node));
|
.querySelectorAll('[data-hide="' + key + '"]')
|
||||||
|
.forEach((node) => this.setDisplay(node));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,6 +39,6 @@ let LocalStorageTools = new class {
|
||||||
let key = node.dataset.hide;
|
let key = node.dataset.hide;
|
||||||
let value = window.localStorage.getItem(key);
|
let value = window.localStorage.getItem(key);
|
||||||
|
|
||||||
BookWyrm.addRemoveClass(node, 'is-hidden', value);
|
BookWyrm.addRemoveClass(node, "is-hidden", value);
|
||||||
}
|
}
|
||||||
}();
|
})();
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
/* exported StatusCache */
|
/* exported StatusCache */
|
||||||
/* globals BookWyrm */
|
/* globals BookWyrm */
|
||||||
|
|
||||||
let StatusCache = new class {
|
let StatusCache = new (class {
|
||||||
constructor() {
|
constructor() {
|
||||||
document.querySelectorAll('[data-cache-draft]')
|
document
|
||||||
.forEach(t => t.addEventListener('change', this.updateDraft.bind(this)));
|
.querySelectorAll("[data-cache-draft]")
|
||||||
|
.forEach((t) => t.addEventListener("change", this.updateDraft.bind(this)));
|
||||||
|
|
||||||
document.querySelectorAll('[data-cache-draft]')
|
document.querySelectorAll("[data-cache-draft]").forEach((t) => this.populateDraft(t));
|
||||||
.forEach(t => this.populateDraft(t));
|
|
||||||
|
|
||||||
document.querySelectorAll('.submit-status')
|
document
|
||||||
.forEach(button => button.addEventListener(
|
.querySelectorAll(".submit-status")
|
||||||
'submit',
|
.forEach((button) => button.addEventListener("submit", this.submitStatus.bind(this)));
|
||||||
this.submitStatus.bind(this))
|
|
||||||
);
|
|
||||||
|
|
||||||
document.querySelectorAll('.form-rate-stars label.icon')
|
document
|
||||||
.forEach(button => button.addEventListener('click', this.toggleStar.bind(this)));
|
.querySelectorAll(".form-rate-stars label.icon")
|
||||||
|
.forEach((button) => button.addEventListener("click", this.toggleStar.bind(this)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,25 +79,26 @@ let StatusCache = new class {
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
BookWyrm.addRemoveClass(form, 'is-processing', true);
|
BookWyrm.addRemoveClass(form, "is-processing", true);
|
||||||
trigger.setAttribute('disabled', null);
|
trigger.setAttribute("disabled", null);
|
||||||
|
|
||||||
BookWyrm.ajaxPost(form).finally(() => {
|
BookWyrm.ajaxPost(form)
|
||||||
// Change icon to remove ongoing activity on the current UI.
|
.finally(() => {
|
||||||
// Enable back the element used to submit the form.
|
// Change icon to remove ongoing activity on the current UI.
|
||||||
BookWyrm.addRemoveClass(form, 'is-processing', false);
|
// Enable back the element used to submit the form.
|
||||||
trigger.removeAttribute('disabled');
|
BookWyrm.addRemoveClass(form, "is-processing", false);
|
||||||
})
|
trigger.removeAttribute("disabled");
|
||||||
.then(response => {
|
})
|
||||||
if (!response.ok) {
|
.then((response) => {
|
||||||
throw new Error();
|
if (!response.ok) {
|
||||||
}
|
throw new Error();
|
||||||
this.submitStatusSuccess(form);
|
}
|
||||||
})
|
this.submitStatusSuccess(form);
|
||||||
.catch(error => {
|
})
|
||||||
console.warn(error);
|
.catch((error) => {
|
||||||
this.announceMessage('status-error-message');
|
console.warn(error);
|
||||||
});
|
this.announceMessage("status-error-message");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -112,12 +112,16 @@ let StatusCache = new class {
|
||||||
let copy = element.cloneNode(true);
|
let copy = element.cloneNode(true);
|
||||||
|
|
||||||
copy.id = null;
|
copy.id = null;
|
||||||
element.insertAdjacentElement('beforebegin', copy);
|
element.insertAdjacentElement("beforebegin", copy);
|
||||||
|
|
||||||
BookWyrm.addRemoveClass(copy, 'is-hidden', false);
|
BookWyrm.addRemoveClass(copy, "is-hidden", false);
|
||||||
setTimeout(function() {
|
setTimeout(
|
||||||
copy.remove();
|
function () {
|
||||||
}, 10000, copy);
|
copy.remove();
|
||||||
|
},
|
||||||
|
10000,
|
||||||
|
copy
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -131,8 +135,9 @@ let StatusCache = new class {
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
||||||
// Clear localstorage
|
// Clear localstorage
|
||||||
form.querySelectorAll('[data-cache-draft]')
|
form.querySelectorAll("[data-cache-draft]").forEach((node) =>
|
||||||
.forEach(node => window.localStorage.removeItem(node.dataset.cacheDraft));
|
window.localStorage.removeItem(node.dataset.cacheDraft)
|
||||||
|
);
|
||||||
|
|
||||||
// Close modals
|
// Close modals
|
||||||
let modal = form.closest(".modal.is-active");
|
let modal = form.closest(".modal.is-active");
|
||||||
|
@ -142,8 +147,11 @@ let StatusCache = new class {
|
||||||
|
|
||||||
// Update shelve buttons
|
// Update shelve buttons
|
||||||
if (form.reading_status) {
|
if (form.reading_status) {
|
||||||
document.querySelectorAll("[data-shelve-button-book='" + form.book.value +"']")
|
document
|
||||||
.forEach(button => this.cycleShelveButtons(button, form.reading_status.value));
|
.querySelectorAll("[data-shelve-button-book='" + form.book.value + "']")
|
||||||
|
.forEach((button) =>
|
||||||
|
this.cycleShelveButtons(button, form.reading_status.value)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -156,7 +164,7 @@ let StatusCache = new class {
|
||||||
document.querySelector("[data-controls=" + reply.id + "]").click();
|
document.querySelector("[data-controls=" + reply.id + "]").click();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.announceMessage('status-success-message');
|
this.announceMessage("status-success-message");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -172,8 +180,9 @@ let StatusCache = new class {
|
||||||
let next_identifier = shelf.dataset.shelfNext;
|
let next_identifier = shelf.dataset.shelfNext;
|
||||||
|
|
||||||
// Set all buttons to hidden
|
// Set all buttons to hidden
|
||||||
button.querySelectorAll("[data-shelf-identifier]")
|
button
|
||||||
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", true));
|
.querySelectorAll("[data-shelf-identifier]")
|
||||||
|
.forEach((item) => BookWyrm.addRemoveClass(item, "is-hidden", true));
|
||||||
|
|
||||||
// Button that should be visible now
|
// Button that should be visible now
|
||||||
let next = button.querySelector("[data-shelf-identifier=" + next_identifier + "]");
|
let next = button.querySelector("[data-shelf-identifier=" + next_identifier + "]");
|
||||||
|
@ -183,15 +192,17 @@ let StatusCache = new class {
|
||||||
|
|
||||||
// ------ update the dropdown buttons
|
// ------ update the dropdown buttons
|
||||||
// Remove existing hidden class
|
// Remove existing hidden class
|
||||||
button.querySelectorAll("[data-shelf-dropdown-identifier]")
|
button
|
||||||
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", false));
|
.querySelectorAll("[data-shelf-dropdown-identifier]")
|
||||||
|
.forEach((item) => BookWyrm.addRemoveClass(item, "is-hidden", false));
|
||||||
|
|
||||||
// Remove existing disabled states
|
// Remove existing disabled states
|
||||||
|
|
||||||
button.querySelectorAll("[data-shelf-dropdown-identifier] button")
|
button
|
||||||
.forEach(item => item.disabled = false);
|
.querySelectorAll("[data-shelf-dropdown-identifier] button")
|
||||||
|
.forEach((item) => (item.disabled = false));
|
||||||
|
|
||||||
next_identifier = next_identifier == 'complete' ? 'read' : next_identifier;
|
next_identifier = next_identifier == "complete" ? "read" : next_identifier;
|
||||||
|
|
||||||
// Disable the current state
|
// Disable the current state
|
||||||
button.querySelector(
|
button.querySelector(
|
||||||
|
@ -206,8 +217,9 @@ let StatusCache = new class {
|
||||||
BookWyrm.addRemoveClass(main_button, "is-hidden", true);
|
BookWyrm.addRemoveClass(main_button, "is-hidden", true);
|
||||||
|
|
||||||
// Just hide the other two menu options, idk what to do with them
|
// Just hide the other two menu options, idk what to do with them
|
||||||
button.querySelectorAll("[data-extra-options]")
|
button
|
||||||
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", true));
|
.querySelectorAll("[data-extra-options]")
|
||||||
|
.forEach((item) => BookWyrm.addRemoveClass(item, "is-hidden", true));
|
||||||
|
|
||||||
// Close menu
|
// Close menu
|
||||||
let menu = button.querySelector("details[open]");
|
let menu = button.querySelector("details[open]");
|
||||||
|
@ -235,5 +247,4 @@ let StatusCache = new class {
|
||||||
halfStar.checked = "checked";
|
halfStar.checked = "checked";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}();
|
})();
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db.models import signals, Count, Q
|
from django.db import transaction
|
||||||
|
from django.db.models import signals, Count, Q, Case, When, IntegerField
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.redis_store import RedisStore, r
|
from bookwyrm.redis_store import RedisStore, r
|
||||||
|
@ -29,6 +30,7 @@ class SuggestedUsers(RedisStore):
|
||||||
|
|
||||||
def get_counts_from_rank(self, rank): # pylint: disable=no-self-use
|
def get_counts_from_rank(self, rank): # pylint: disable=no-self-use
|
||||||
"""calculate mutuals count and shared books count from rank"""
|
"""calculate mutuals count and shared books count from rank"""
|
||||||
|
# pylint: disable=c-extension-no-member
|
||||||
return {
|
return {
|
||||||
"mutuals": math.floor(rank),
|
"mutuals": math.floor(rank),
|
||||||
# "shared_books": int(1 / (-1 * (rank % 1 - 1))) - 1,
|
# "shared_books": int(1 / (-1 * (rank % 1 - 1))) - 1,
|
||||||
|
@ -84,24 +86,17 @@ class SuggestedUsers(RedisStore):
|
||||||
def get_suggestions(self, user, local=False):
|
def get_suggestions(self, user, local=False):
|
||||||
"""get suggestions"""
|
"""get suggestions"""
|
||||||
values = self.get_store(self.store_id(user), withscores=True)
|
values = self.get_store(self.store_id(user), withscores=True)
|
||||||
results = []
|
annotations = [
|
||||||
|
When(pk=int(pk), then=self.get_counts_from_rank(score)["mutuals"])
|
||||||
|
for (pk, score) in values
|
||||||
|
]
|
||||||
# annotate users with mutuals and shared book counts
|
# annotate users with mutuals and shared book counts
|
||||||
for user_id, rank in values:
|
users = models.User.objects.filter(
|
||||||
counts = self.get_counts_from_rank(rank)
|
is_active=True, bookwyrm_user=True, id__in=[pk for (pk, _) in values]
|
||||||
try:
|
).annotate(mutuals=Case(*annotations, output_field=IntegerField(), default=0))
|
||||||
user = models.User.objects.get(
|
if local:
|
||||||
id=user_id, is_active=True, bookwyrm_user=True
|
users = users.filter(local=True)
|
||||||
)
|
return users.order_by("-mutuals")[:5]
|
||||||
except models.User.DoesNotExist as err:
|
|
||||||
# if this happens, the suggestions are janked way up
|
|
||||||
logger.exception(err)
|
|
||||||
continue
|
|
||||||
user.mutuals = counts["mutuals"]
|
|
||||||
if (local and user.local) or not local:
|
|
||||||
results.append(user)
|
|
||||||
if len(results) >= 5:
|
|
||||||
break
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def get_annotated_users(viewer, *args, **kwargs):
|
def get_annotated_users(viewer, *args, **kwargs):
|
||||||
|
@ -119,16 +114,17 @@ def get_annotated_users(viewer, *args, **kwargs):
|
||||||
),
|
),
|
||||||
distinct=True,
|
distinct=True,
|
||||||
),
|
),
|
||||||
# shared_books=Count(
|
# pylint: disable=line-too-long
|
||||||
# "shelfbook",
|
# shared_books=Count(
|
||||||
# filter=Q(
|
# "shelfbook",
|
||||||
# ~Q(id=viewer.id),
|
# filter=Q(
|
||||||
# shelfbook__book__parent_work__in=[
|
# ~Q(id=viewer.id),
|
||||||
# s.book.parent_work for s in viewer.shelfbook_set.all()
|
# shelfbook__book__parent_work__in=[
|
||||||
# ],
|
# s.book.parent_work for s in viewer.shelfbook_set.all()
|
||||||
# ),
|
# ],
|
||||||
# distinct=True,
|
# ),
|
||||||
# ),
|
# distinct=True,
|
||||||
|
# ),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -197,7 +193,7 @@ def update_user(sender, instance, created, update_fields=None, **kwargs):
|
||||||
"""an updated user, neat"""
|
"""an updated user, neat"""
|
||||||
# a new user is found, create suggestions for them
|
# a new user is found, create suggestions for them
|
||||||
if created and instance.local:
|
if created and instance.local:
|
||||||
rerank_suggestions_task.delay(instance.id)
|
transaction.on_commit(lambda: update_new_user_command(instance.id))
|
||||||
|
|
||||||
# we know what fields were updated and discoverability didn't change
|
# we know what fields were updated and discoverability didn't change
|
||||||
if not instance.bookwyrm_user or (
|
if not instance.bookwyrm_user or (
|
||||||
|
@ -217,6 +213,11 @@ def update_user(sender, instance, created, update_fields=None, **kwargs):
|
||||||
remove_user_task.delay(instance.id)
|
remove_user_task.delay(instance.id)
|
||||||
|
|
||||||
|
|
||||||
|
def update_new_user_command(instance_id):
|
||||||
|
"""wait for transaction to complete"""
|
||||||
|
rerank_suggestions_task.delay(instance_id)
|
||||||
|
|
||||||
|
|
||||||
@receiver(signals.post_save, sender=models.FederatedServer)
|
@receiver(signals.post_save, sender=models.FederatedServer)
|
||||||
def domain_level_update(sender, instance, created, update_fields=None, **kwargs):
|
def domain_level_update(sender, instance, created, update_fields=None, **kwargs):
|
||||||
"""remove users on a domain block"""
|
"""remove users on a domain block"""
|
||||||
|
|
141
bookwyrm/templates/about/about.html
Normal file
141
bookwyrm/templates/about/about.html
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
{% extends 'about/layout.html' %}
|
||||||
|
{% load humanize %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load utilities %}
|
||||||
|
{% load bookwyrm_tags %}
|
||||||
|
{% load cache %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% trans "About" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block about_content %}
|
||||||
|
{# seven day cache #}
|
||||||
|
{% cache 604800 about_page %}
|
||||||
|
{% get_book_superlatives as superlatives %}
|
||||||
|
<section class="content pb-4">
|
||||||
|
<h2>
|
||||||
|
{% blocktrans with site_name=site.name %}Welcome to {{ site_name }}!{% endblocktrans %}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="subtitle notification has-background-primary-light">
|
||||||
|
{% blocktrans trimmed with site_name=site.name %}
|
||||||
|
{{ site_name }} is part of <em>BookWyrm</em>, a network of independent, self-directed communities for readers.
|
||||||
|
While you can interact seemlessly with users anywhere in the <a href="https://joinbookwyrm.com/instances/" target="_blank">BookWyrm network</a>, this community is unique.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
{% if top_rated %}
|
||||||
|
{% with book=superlatives.top_rated.default_edition rating=top_rated.rating %}
|
||||||
|
<div class="column is-one-third is-flex">
|
||||||
|
<div class="media notification">
|
||||||
|
<div class="media-left">
|
||||||
|
<a href="{{ book.local_path }}">
|
||||||
|
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
{% blocktrans trimmed with title=book|book_title book_path=book.local_path site_name=site.name rating=rating|floatformat:1 %}
|
||||||
|
<a href="{{ book_path }}"><em>{{ title }}</em></a> is {{ site_name }}'s most beloved book, with an average rating of {{ rating }} out of 5.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if wanted %}
|
||||||
|
{% with book=superlatives.wanted.default_edition %}
|
||||||
|
<div class="column is-one-third is-flex">
|
||||||
|
<div class="media notification">
|
||||||
|
<div class="media-left">
|
||||||
|
<a href="{{ book.local_path }}">
|
||||||
|
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
{% blocktrans trimmed with title=book|book_title book_path=book.local_path site_name=site.name %}
|
||||||
|
More {{ site_name }} users want to read <a href="{{ book_path }}"><em>{{ title }}</em></a> than any other book.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if controversial %}
|
||||||
|
{% with book=superlatives.controversial.default_edition %}
|
||||||
|
<div class="column is-one-third is-flex">
|
||||||
|
<div class="media notification">
|
||||||
|
<div class="media-left">
|
||||||
|
<a href="{{ book.local_path }}">
|
||||||
|
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
{% blocktrans trimmed with title=book|book_title book_path=book.local_path site_name=site.name %}
|
||||||
|
<a href="{{ book_path }}"><em>{{ title }}</em></a> has the most divisive ratings of any book on {{ site_name }}.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% trans "Track your reading, talk about books, write reviews, and discover what to read next. Always ad-free, anti-corporate, and community-oriented, BookWyrm is human-scale software, designed to stay small and personal. If you have feature requests, bug reports, or grand dreams, <a href='https://joinbookwyrm.com/get-involved' target='_blank'>reach out</a> and make yourself heard." %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="block">
|
||||||
|
<header class="content">
|
||||||
|
<h2 class="title is-3">{% trans "Meet your admins" %}</h2>
|
||||||
|
<p>
|
||||||
|
{% url "conduct" as coc_path %}
|
||||||
|
{% blocktrans with site_name=site.name %}
|
||||||
|
{{ site_name }}'s moderators and administrators keep the site up and running, enforce the <a href="coc_path">code of conduct</a>, and respond when users report spam and bad behavior.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
{% for user in admins %}
|
||||||
|
<div class="column">
|
||||||
|
<div class="card is-stretchable">
|
||||||
|
{% with role=user.groups.first.name %}
|
||||||
|
<div class="card-header {% if role == "moderator" %}has-background-info-light{% else %}has-background-success-light{% endif %}">
|
||||||
|
<span class="card-header-title is-size-7 pt-1 pb-1">
|
||||||
|
{% if role == "moderator" %}
|
||||||
|
{% trans "Moderator" %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Admin" %}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="cord-content p-5">
|
||||||
|
{% include 'user/user_preview.html' with user=user %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if request.user.is_authenticated and user.id != request.user.id %}
|
||||||
|
<div class="has-background-white-bis card-footer">
|
||||||
|
<div class="card-footer-item">
|
||||||
|
{% include 'snippets/follow_button.html' with user=user minimal=True %}
|
||||||
|
</div>
|
||||||
|
<div class="card-footer-item">
|
||||||
|
<a href="{% url 'direct-messages-user' user|username %}">{% trans "Send direct message" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endcache %}
|
||||||
|
{% endblock %}
|
15
bookwyrm/templates/about/conduct.html
Normal file
15
bookwyrm/templates/about/conduct.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends 'about/layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Code of Conduct" %}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block about_content %}
|
||||||
|
<div class="block content">
|
||||||
|
<h2>{% trans "Code of Conduct" %}</h2>
|
||||||
|
<div class="content">
|
||||||
|
{{ site.code_of_conduct | safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
57
bookwyrm/templates/about/layout.html
Normal file
57
bookwyrm/templates/about/layout.html
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
{% extends 'landing/layout.html' %}
|
||||||
|
{% load humanize %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block about_panel %}
|
||||||
|
<div class="box">
|
||||||
|
{% include "snippets/about.html" with size="m" %}
|
||||||
|
{% if active_users %}
|
||||||
|
<ul>
|
||||||
|
<li class="tag is-size-6">
|
||||||
|
<span class="mr-1">{% trans "Active users:" %}</span>
|
||||||
|
<strong>{{ active_users|intcomma }}</strong>
|
||||||
|
</li>
|
||||||
|
<li class="tag is-size-6">
|
||||||
|
<span class="mr-1">{% trans "Statuses posted:" %}</span>
|
||||||
|
<strong>{{ status_count|intcomma }}</strong>
|
||||||
|
</li>
|
||||||
|
<li class="tag is-size-6">
|
||||||
|
<span class="mr-1">{% trans "Software version:" %}</span>
|
||||||
|
<strong>{{ version }}</strong>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
<div class="block columns pt-4">
|
||||||
|
<nav class="menu column is-one-quarter">
|
||||||
|
<h2 class="menu-label">{% blocktrans with site_name=site.name %}About {{ site_name }}{% endblocktrans %}</h2>
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li>
|
||||||
|
{% url 'about' as path %}
|
||||||
|
<a href="{{ path }}" {% if request.path in path %}class="is-active"{% endif %}>
|
||||||
|
{% trans "About" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{% url 'conduct' as path %}
|
||||||
|
<a href="{{ path }}" {% if request.path in path %}class="is-active"{% endif %}>
|
||||||
|
{% trans "Code of Conduct" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{% url 'privacy' as path %}
|
||||||
|
<a href="{{ path }}" {% if request.path in path %}class="is-active"{% endif %}>
|
||||||
|
{% trans "Privacy Policy" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
{% block about_content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
15
bookwyrm/templates/about/privacy.html
Normal file
15
bookwyrm/templates/about/privacy.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends 'about/layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Privacy Policy" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block about_content %}
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h2 class="title">{% trans "Privacy Policy" %}</h2>
|
||||||
|
<div class="content">
|
||||||
|
{{ site.privacy_policy | safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
298
bookwyrm/templates/annual_summary/layout.html
Normal file
298
bookwyrm/templates/annual_summary/layout.html
Normal file
|
@ -0,0 +1,298 @@
|
||||||
|
{% extends 'layout.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block title %}{% blocktrans %}{{ year }} in the books{% endblocktrans %}{% endblock %}
|
||||||
|
|
||||||
|
{% block head_links %}
|
||||||
|
<link rel="stylesheet" href="{% static "css/vendor/dm_serif_display.css" %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% with display_name=summary_user.display_name %}
|
||||||
|
{% if user == summary_user %}
|
||||||
|
<div class="columns">
|
||||||
|
{% with year=paginated_years|first %}
|
||||||
|
{% if year %}
|
||||||
|
<div class="column">
|
||||||
|
<a href="{% url 'annual-summary' summary_user.localname year %}">
|
||||||
|
<span class="icon icon-arrow-left" aria-hidden="true"></span>
|
||||||
|
{{ year }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with year=paginated_years|last %}
|
||||||
|
{% if year %}
|
||||||
|
<div class="column has-text-right">
|
||||||
|
<a href="{% url 'annual-summary' summary_user.localname year %}">
|
||||||
|
{{ year }}
|
||||||
|
<span class="icon icon-arrow-right" aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h1 class="title is-1 is-serif has-text-centered">
|
||||||
|
📚✨
|
||||||
|
{% blocktrans %}{{ year }} <em>in the books</em>{% endblocktrans %}
|
||||||
|
✨📚
|
||||||
|
</h1>
|
||||||
|
<p class="subtitle is-3 is-serif has-text-centered mb-5">
|
||||||
|
{% blocktrans %}<em>{{ display_name }}’s</em> year of reading{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary class="has-text-centered">
|
||||||
|
<span role="heading" aria-level="2" class="title is-6 has-text-success-dark">
|
||||||
|
{% trans "Share this page" %}
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div class="columns mt-3">
|
||||||
|
<div class="column is-three-fifths is-offset-one-fifth">
|
||||||
|
|
||||||
|
{% if year_key %}
|
||||||
|
<div class="horizontal-copy mb-5">
|
||||||
|
<textarea
|
||||||
|
rows="1"
|
||||||
|
readonly
|
||||||
|
class="textarea is-small"
|
||||||
|
aria-labelledby="embed-label"
|
||||||
|
data-copytext
|
||||||
|
data-copytext-label="{% trans 'Copy address' %}"
|
||||||
|
data-copytext-success="{% trans 'Copied!' %}"
|
||||||
|
>{{ request.scheme|add:"://"|add:request.get_host|add:request.path }}?key={{ year_key }}</textarea>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user == summary_user %}
|
||||||
|
{% if year_key %}
|
||||||
|
<div class="columns mb-2">
|
||||||
|
<div class="column pb-0">
|
||||||
|
<p>{% trans "Sharing status: <strong>public with key</strong>" %}</p>
|
||||||
|
<p>{% trans "The page can be seen by anyone with the complete address." %}</p>
|
||||||
|
</div>
|
||||||
|
<form class="column pb-0 is-narrow" method="post" action="{% url "summary-revoke-key" %}" id="revoke-key">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="year" value="{{ year }}" />
|
||||||
|
<button class="button is-danger is-outlined" type="submit">{% trans "Make page private" %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column pb-0">
|
||||||
|
<p>{% trans "Sharing status: <strong>private</strong>" %}</p>
|
||||||
|
<p>{% trans "The page is private, only you can see it." %}</p>
|
||||||
|
</div>
|
||||||
|
<form class="column pb-0 is-narrow" method="post" action="{% url "summary-add-key" %}" id="add-key">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="year" value="{{ year }}" />
|
||||||
|
<button class="button is-primary is-outlined" type="submit">{% trans "Make page public" %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<p class="help">{% trans "When you make your page private, the old key won’t give access to the page anymore. A new key will be created if the page is once again made public." %}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div class="columns mt-1">
|
||||||
|
<div class="column is-one-fifth is-offset-two-fifths">
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not books %}
|
||||||
|
<p class="has-text-centered is-size-5">{% blocktrans %}Sadly {{ display_name }} didn’t finish any books in {{ year }}{% endblocktrans %}</p>
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<div class="columns is-mobile">
|
||||||
|
<div class="column is-8 is-offset-2 has-text-centered">
|
||||||
|
<h2 class="title is-3 is-serif">
|
||||||
|
{% blocktrans trimmed count counter=books_total with pages_total=pages_total|intcomma %}
|
||||||
|
In {{ year }}, {{ display_name }} read {{ books_total }} book<br />for a total of {{ pages_total }} pages!
|
||||||
|
{% plural %}
|
||||||
|
In {{ year }}, {{ display_name }} read {{ books_total }} books<br />for a total of {{ pages_total }} pages!
|
||||||
|
{% endblocktrans %}
|
||||||
|
</h2>
|
||||||
|
<p class="subtitle is-5">{% trans "That’s great!" %}</p>
|
||||||
|
|
||||||
|
<p class="title is-4 is-serif">
|
||||||
|
{% blocktrans with pages=pages_average|intcomma %}That makes an average of {{ pages }} pages per book.{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if no_page_number %}
|
||||||
|
<p class="subtitle is-6">
|
||||||
|
{% blocktrans trimmed count counter=no_page_number %}
|
||||||
|
({{ no_page_number }} book doesn’t have pages)
|
||||||
|
{% plural %}
|
||||||
|
({{ no_page_number }} books don’t have pages)
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if book_pages_lowest and book_pages_highest %}
|
||||||
|
<div class="columns is-align-items-center mt-5">
|
||||||
|
<div class="column is-2 is-offset-1">
|
||||||
|
<a href="{{ book_pages_lowest.local_path }}">{% include 'snippets/book_cover.html' with book=book_pages_lowest cover_class='is-w-auto-tablet is-h-l-mobile' size='xxlarge' %}</a>
|
||||||
|
</div>
|
||||||
|
<div class="column is-3">
|
||||||
|
{% trans "Their shortest read this year…" %}
|
||||||
|
<p class="title is-4 is-serif is-italic">
|
||||||
|
<a href="{{ book_pages_lowest.local_path }}" class="has-text-success-dark">
|
||||||
|
{{ book_pages_lowest.title }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% if book_pages_lowest.authors.exists %}
|
||||||
|
<p class="subtitle is-5 mb-2">{% trans "by" %}
|
||||||
|
{% include 'snippets/authors.html' with book=book_pages_lowest link_class="has-text-success-dark" %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<p class="subtitle is-6">
|
||||||
|
{% with pages=book_pages_lowest.pages %}
|
||||||
|
{% blocktrans %}<strong>{{ pages }}</strong> pages{% endblocktrans%}
|
||||||
|
{% endwith %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="column is-2">
|
||||||
|
<a href="{{ book_pages_highest.local_path }}">{% include 'snippets/book_cover.html' with book=book_pages_highest cover_class='is-w-auto-tablet is-h-l-mobile' size='xxlarge' %}</a>
|
||||||
|
</div>
|
||||||
|
<div class="column is-3">
|
||||||
|
{% trans "…and the longest" %}
|
||||||
|
<p class="title is-4 is-serif is-italic">
|
||||||
|
<a href="{{ book_pages_lowest.local_path }}" class="has-text-success-dark">
|
||||||
|
{{ book_pages_highest.title }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% if book_pages_highest.authors.exists %}
|
||||||
|
<p class="subtitle is-5 mb-2">{% trans "by" %}
|
||||||
|
{% include 'snippets/authors.html' with book=book_pages_highest link_class="has-text-success-dark" %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<p class="subtitle is-6">
|
||||||
|
{% with pages=book_pages_highest.pages %}
|
||||||
|
{% blocktrans %}<strong>{{ pages }}</strong> pages{% endblocktrans%}
|
||||||
|
{% endwith %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-one-fifth is-offset-two-fifths">
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if goal_status and goal_status.percent >= 100 %}
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column has-text-centered">
|
||||||
|
<h2 class="title is-3 is-serif">
|
||||||
|
{% with goal=goal_status.goal goal_percent=goal_status.percent %}
|
||||||
|
{% blocktrans trimmed count counter=goal %}
|
||||||
|
{{ display_name }} set a goal of reading {{ goal }} book in {{ year }},<br />
|
||||||
|
and achieved {{ goal_percent }}% of that goal
|
||||||
|
{% plural %}
|
||||||
|
{{ display_name }} set a goal of reading {{ goal }} books in {{ year }},<br />
|
||||||
|
and achieved {{ goal_percent }}% of that goal
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endwith %}
|
||||||
|
</h2>
|
||||||
|
<p class="subtitle is-5">{% trans "Way to go!" %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-one-fifth is-offset-two-fifths">
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if ratings_total > 0 %}
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column has-text-centered">
|
||||||
|
<h2 class="title is-3 is-serif">
|
||||||
|
{% blocktrans trimmed count counter=ratings_total %}
|
||||||
|
{{ display_name }} left {{ ratings_total }} rating, <br />their average rating is {{ rating_average }}
|
||||||
|
{% plural %}
|
||||||
|
{{ display_name }} left {{ ratings_total }} ratings, <br />their average rating is {{ rating_average }}
|
||||||
|
{% endblocktrans %}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columns is-align-items-center">
|
||||||
|
<div class="column is-2 is-offset-4">
|
||||||
|
<a href="{{ book_rating_highest.book.local_path }}">{% include 'snippets/book_cover.html' with book=book_rating_highest.book cover_class='is-w-xl-tablet is-h-l-mobile' size='xxlarge' %}</a>
|
||||||
|
</div>
|
||||||
|
{% if book_rating_highest %}
|
||||||
|
<div class="column is-4">
|
||||||
|
{% trans "Their best rated review" %}
|
||||||
|
<p class="title is-4 is-serif is-italic">
|
||||||
|
<a href="{{ book_rating_highest.book.local_path }}" class="has-text-success-dark">
|
||||||
|
{{ book_rating_highest.book.title }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% if book_rating_highest.book.authors.exists %}
|
||||||
|
<p class="subtitle is-5 mb-2">{% trans "by" %}
|
||||||
|
{% include 'snippets/authors.html' with book=book_rating_highest.book link_class="has-text-success-dark" %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<p class="subtitle is-6">
|
||||||
|
{% with rating=book_rating_highest.rating|floatformat %}
|
||||||
|
{% blocktrans %}Their rating: <strong>{{ rating }}</strong>{% endblocktrans%}
|
||||||
|
{% endwith %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-one-fifth is-offset-two-fifths">
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column has-text-centered">
|
||||||
|
<h2 class="title is-3 is-serif">
|
||||||
|
{% blocktrans %}All the books {{ display_name }} read in {{ year }}{% endblocktrans %}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-10 is-offset-1">
|
||||||
|
<div class="books-grid">
|
||||||
|
{% for book in books %}
|
||||||
|
{% if books_total > 12 and book.id in best_ratings_books_ids %}
|
||||||
|
<a href="{{ book.local_path }}" class="has-text-centered is-big has-text-success-dark">
|
||||||
|
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto' size='xxlarge' %}
|
||||||
|
<span class="book-title is-serif is-size-5">
|
||||||
|
{{ book.title }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ book.local_path }}" class="has-text-centered has-text-success-dark">
|
||||||
|
{% include 'snippets/book_cover.html' with book=book cover_class='is-w-auto' size='xlarge' %}
|
||||||
|
<span class="book-title is-serif is-size-6">
|
||||||
|
{{ book.title }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
|
@ -92,10 +92,11 @@
|
||||||
{% 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 %}
|
||||||
{% with controls_text="ol_sync" controls_uid=author.id %}
|
<button class="button is-small" type="button" data-modal-open="openlibrary_sync">
|
||||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_ol_sync" class="is-small" icon_with_text="download" %}
|
<span class="icon icon-download" title="{{ button_text }}"></span>
|
||||||
{% include "author/sync_modal.html" with source="openlibrary.org" source_name="OpenLibrary" %}
|
<span class="is-sr-only-mobile">{{ button_text }}</span>
|
||||||
{% endwith %}
|
</button>
|
||||||
|
{% include "author/sync_modal.html" with source="openlibrary.org" source_name="OpenLibrary" id="openlibrary_sync" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -107,10 +108,11 @@
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||||
{% with controls_text="iv_sync" controls_uid=author.id %}
|
<button class="button is-small" type="button" data-modal-open="inventaire_sync">
|
||||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_iv_sync" class="is-small" icon_with_text="download" %}
|
<span class="icon icon-download" title="{{ button_text }}"></span>
|
||||||
{% include "author/sync_modal.html" with source="inventaire.io" source_name="Inventaire" %}
|
<span class="is-sr-only-mobile">{{ button_text }}</span>
|
||||||
{% endwith %}
|
</button>
|
||||||
|
{% include "author/sync_modal.html" with source="inventaire.io" source_name="Inventaire" id="inventaire_sync" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -19,12 +19,8 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal-footer %}
|
{% block modal-footer %}
|
||||||
<button class="button is-primary" type="submit">
|
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button>
|
||||||
<span>{% trans "Confirm" %}</span>
|
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||||
</button>
|
|
||||||
|
|
||||||
{% trans "Cancel" as button_text %}
|
|
||||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal-form-close %}</form>{% endblock %}
|
{% block modal-form-close %}</form>{% endblock %}
|
||||||
|
|
|
@ -61,24 +61,53 @@
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-one-fifth">
|
<div class="column is-one-fifth">
|
||||||
{% include 'snippets/book_cover.html' with size='xxlarge' size_mobile='medium' book=book cover_class='is-h-m-mobile' %}
|
{% if not book.cover %}
|
||||||
|
{% if user_authenticated %}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="cover-container no-cover is-h-m-mobile"
|
||||||
|
data-modal-open="add_cover_{{ book.id }}"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="book-cover"
|
||||||
|
src="{% static "images/no_cover.jpg" %}"
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<span class="cover-caption">
|
||||||
|
<span>{{ book.alt_text }}</span>
|
||||||
|
<span>{% trans "Click to add cover" %}</span>
|
||||||
|
</span>
|
||||||
|
<span class="button-invisible-overlay has-text-centered">
|
||||||
|
{% trans "Click to add cover" %}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{% join "add_cover" book.id as modal_id %}
|
||||||
|
{% include 'book/cover_add_modal.html' with id=modal_id %}
|
||||||
|
{% if request.GET.cover_error %}
|
||||||
|
<p class="help is-danger">{% trans "Failed to load cover" %}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m-mobile' %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if book.cover %}
|
||||||
|
<button type="button" data-modal-open="cover_show_modal" class="cover-container is-h-m-mobile is-relative">
|
||||||
|
{% include 'snippets/book_cover.html' with size='xxlarge' size_mobile='medium' book=book cover_class='is-h-m-mobile' %}
|
||||||
|
<span class="button-invisible-overlay has-text-centered">
|
||||||
|
{% trans "Click to enlarge" %}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{% include 'book/cover_show_modal.html' with book=book id="cover_show_modal" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% include 'snippets/rate_action.html' with user=request.user book=book %}
|
{% include 'snippets/rate_action.html' with user=request.user book=book %}
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{% include 'snippets/shelve_button/shelve_button.html' %}
|
{% include 'snippets/shelve_button/shelve_button.html' %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if user_authenticated and not book.cover %}
|
|
||||||
<div class="block">
|
|
||||||
{% trans "Add cover" as button_text %}
|
|
||||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="add_cover" controls_uid=book.id focus="modal_title_add_cover" class="is-small" %}
|
|
||||||
{% include 'book/cover_modal.html' with book=book controls_text="add_cover" controls_uid=book.id %}
|
|
||||||
{% if request.GET.cover_error %}
|
|
||||||
<p class="help is-danger">{% trans "Failed to load cover" %}</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<section class="is-clipped">
|
<section class="is-clipped">
|
||||||
{% with book=book %}
|
{% with book=book %}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
@ -93,23 +122,30 @@
|
||||||
{% 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">{% trans "View on OpenLibrary" %}</a>
|
<a href="{{ book.openlibrary_link }}" target="_blank" rel="noopener">
|
||||||
|
{% trans "View on OpenLibrary" %}
|
||||||
|
</a>
|
||||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||||
{% with controls_text="ol_sync" controls_uid=book.id %}
|
<button class="button is-small" type="button" data-modal-open="openlibrary_sync">
|
||||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_ol_sync" class="is-small" icon_with_text="download" %}
|
<span class="icon icon-download" title="{{ button_text }}"></span>
|
||||||
{% include "book/sync_modal.html" with source="openlibrary.org" source_name="OpenLibrary" %}
|
<span class="is-sr-only-mobile">{{ button_text }}</span>
|
||||||
{% endwith %}
|
</button>
|
||||||
|
{% include "book/sync_modal.html" with source="openlibrary.org" source_name="OpenLibrary" id="openlibrary_sync" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if book.inventaire_id %}
|
{% if book.inventaire_id %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ book.inventaire_link }}" target="_blank" rel="noopener">{% trans "View on Inventaire" %}</a>
|
<a href="{{ book.inventaire_link }}" target="_blank" rel="noopener">
|
||||||
|
{% trans "View on Inventaire" %}
|
||||||
|
</a>
|
||||||
|
|
||||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||||
{% with controls_text="iv_sync" controls_uid=book.id %}
|
<button class="button is-small" type="button" data-modal-open="inventaire_sync">
|
||||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text focus="modal_title_iv_sync" class="is-small" icon_with_text="download" %}
|
<span class="icon icon-download" title="{{ button_text }}"></span>
|
||||||
{% include "book/sync_modal.html" with source="inventaire.io" source_name="Inventaire" %}
|
<span class="is-sr-only-mobile">{{ button_text }}</span>
|
||||||
{% endwith %}
|
</button>
|
||||||
|
{% include "book/sync_modal.html" with source="inventaire.io" source_name="Inventaire" id="inventaire_sync" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -29,8 +29,7 @@
|
||||||
|
|
||||||
{% block modal-footer %}
|
{% block modal-footer %}
|
||||||
<button class="button is-primary" type="submit">{% trans "Add" %}</button>
|
<button class="button is-primary" type="submit">{% trans "Add" %}</button>
|
||||||
{% trans "Cancel" as button_text %}
|
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block modal-form-close %}</form>{% endblock %}
|
|
||||||
|
|
||||||
|
{% block modal-form-close %}</form>{% endblock %}
|
12
bookwyrm/templates/book/cover_show_modal.html
Normal file
12
bookwyrm/templates/book/cover_show_modal.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<div class="modal" id="{{ id }}">
|
||||||
|
<div class="modal-background" data-modal-close></div><!-- modal background -->
|
||||||
|
<div class="modal-card is-align-items-center" role="dialog" aria-modal="true" tabindex="-1" aria-label="{% trans 'Book cover preview' %}">
|
||||||
|
<div class="cover-container">
|
||||||
|
<img class="book-cover" src="{% get_media_prefix %}{{ book.cover }}" itemprop="thumbnailUrl" alt="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" data-modal-close class="modal-close is-large" aria-label="{% trans 'Close' %}"></button>
|
||||||
|
</div>
|
|
@ -2,11 +2,17 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block modal-title %}{% trans "Delete these read dates?" %}{% endblock %}
|
{% block modal-title %}{% trans "Delete these read dates?" %}{% endblock %}
|
||||||
|
|
||||||
{% block modal-body %}
|
{% block modal-body %}
|
||||||
{% if readthrough.progress_updates|length > 0 %}
|
{% if readthrough.progress_updates|length > 0 %}
|
||||||
{% blocktrans with count=readthrough.progress_updates|length %}You are deleting this readthrough and its {{ count }} associated progress updates.{% endblocktrans %}
|
{% blocktrans trimmed with count=readthrough.progress_updates|length %}
|
||||||
|
You are deleting this readthrough and its {{ count }} associated progress updates.
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "This action cannot be un-done" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal-footer %}
|
{% block modal-footer %}
|
||||||
<form name="delete-readthrough-{{ readthrough.id }}" action="/delete-readthrough" method="POST">
|
<form name="delete-readthrough-{{ readthrough.id }}" action="/delete-readthrough" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
@ -14,7 +20,6 @@
|
||||||
<button class="button is-danger" type="submit">
|
<button class="button is-danger" type="submit">
|
||||||
{% trans "Delete" %}
|
{% trans "Delete" %}
|
||||||
</button>
|
</button>
|
||||||
{% trans "Cancel" as button_text %}
|
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="delete_readthrough" controls_uid=readthrough.id %}
|
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -1,6 +1,7 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
{% load tz %}
|
{% load tz %}
|
||||||
|
{% load utilities %}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div id="hide_edit_readthrough_{{ readthrough.id }}" class="box is-shadowless has-background-white-bis">
|
<div id="hide_edit_readthrough_{{ readthrough.id }}" class="box is-shadowless has-background-white-bis">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
|
@ -10,14 +11,14 @@
|
||||||
{% if readthrough.finish_date or readthrough.progress %}
|
{% if readthrough.finish_date or readthrough.progress %}
|
||||||
<li>
|
<li>
|
||||||
{% if readthrough.finish_date %}
|
{% if readthrough.finish_date %}
|
||||||
{{ readthrough.finish_date | localtime | naturalday }}: {% trans "finished" %}
|
{{ readthrough.finish_date | localtime | naturalday }}: {% trans "finished" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
{% if readthrough.progress_mode == 'PG' %}
|
{% if readthrough.progress_mode == 'PG' %}
|
||||||
{% include 'snippets/page_text.html' with page=readthrough.progress total_pages=book.pages %}
|
{% include 'snippets/page_text.html' with page=readthrough.progress total_pages=book.pages %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ readthrough.progress }}%
|
{{ readthrough.progress }}%
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if readthrough.progress %}
|
{% if readthrough.progress %}
|
||||||
|
@ -47,6 +48,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if readthrough.start_date %}
|
{% if readthrough.start_date %}
|
||||||
<li>{{ readthrough.start_date | localtime | naturalday }}: {% trans "started" %}</li>
|
<li>{{ readthrough.start_date | localtime | naturalday }}: {% trans "started" %}</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -60,7 +62,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
{% trans "Delete these read dates" as button_text %}
|
{% trans "Delete these read dates" as button_text %}
|
||||||
{% include 'snippets/toggle/toggle_button.html' with class="is-small" text=button_text icon="x" controls_text="delete_readthrough" controls_uid=readthrough.id focus="modal_title_delete_readthrough" %}
|
<button class="button is-small" type="button" data-modal-open="delete_readthrough_{{ readthrough.id }}">
|
||||||
|
<span class="icon icon-x" title="{{ button_text }}">
|
||||||
|
<span class="is-sr-only">{{ button_text }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -79,4 +85,5 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% include 'snippets/delete_readthrough_modal.html' with controls_text="delete_readthrough" controls_uid=readthrough.id no_body=True %}
|
{% join "delete_readthrough" readthrough.id as modal_id %}
|
||||||
|
{% include 'book/delete_readthrough_modal.html' with id=modal_id %}
|
||||||
|
|
|
@ -19,12 +19,8 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal-footer %}
|
{% block modal-footer %}
|
||||||
<button class="button is-primary" type="submit">
|
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button>
|
||||||
<span>{% trans "Confirm" %}</span>
|
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||||
</button>
|
|
||||||
|
|
||||||
{% trans "Cancel" as button_text %}
|
|
||||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal-form-close %}</form>{% endblock %}
|
{% block modal-form-close %}</form>{% endblock %}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
class="
|
class="
|
||||||
dropdown control
|
dropdown control
|
||||||
{% if right %}is-right{% endif %}
|
{% if right %}is-right{% endif %}
|
||||||
|
has-text-left
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<summary
|
<summary
|
||||||
|
@ -15,7 +16,7 @@
|
||||||
{% block dropdown-trigger %}{% endblock %}
|
{% block dropdown-trigger %}{% endblock %}
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
<div class="dropdown-menu control">
|
<div class="dropdown-menu">
|
||||||
<ul
|
<ul
|
||||||
id="menu_options_{{ uuid }}"
|
id="menu_options_{{ uuid }}"
|
||||||
class="dropdown-content p-0 is-clipped"
|
class="dropdown-content p-0 is-clipped"
|
||||||
|
|
|
@ -1,40 +1,33 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div
|
|
||||||
role="dialog"
|
<div class="modal {% if active %}is-active{% endif %}" id="{{ id }}">
|
||||||
class="modal {% if active %}is-active{% else %}is-hidden{% endif %}"
|
<div class="modal-background" data-modal-close></div>
|
||||||
id="{{ controls_text }}_{{ controls_uid }}"
|
<div class="modal-card" role="dialog" aria-modal="true" tabindex="-1" aria-described-by="{{ id }}_header">
|
||||||
aria-labelledby="modal_card_title_{{ controls_text }}_{{ controls_uid }}"
|
<header class="modal-card-head">
|
||||||
aria-modal="true"
|
<h2 class="modal-card-title mb-0" id="{{ id }}_header">
|
||||||
>
|
|
||||||
{# @todo Implement focus traps to prevent tabbing out of the modal. #}
|
|
||||||
<div class="modal-background"></div>
|
|
||||||
{% trans "Close" as label %}
|
|
||||||
<div class="modal-card">
|
|
||||||
<header class="modal-card-head" tabindex="0" id="modal_title_{{ controls_text }}_{{ controls_uid }}">
|
|
||||||
<h2 class="modal-card-title is-flex-shrink-1" id="modal_card_title_{{ controls_text }}_{{ controls_uid }}">
|
|
||||||
{% block modal-title %}{% endblock %}
|
{% block modal-title %}{% endblock %}
|
||||||
</h2>
|
</h2>
|
||||||
{% if static %}
|
<button
|
||||||
<a href="/" class="delete">{{ label }}</a>
|
type="button"
|
||||||
{% else %}
|
class="delete"
|
||||||
{% include 'snippets/toggle/toggle_button.html' with label=label class="delete" nonbutton=True %}
|
aria-label="{% trans 'Close' %}"
|
||||||
{% endif %}
|
data-modal-close
|
||||||
|
></button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{% block modal-form-open %}{% endblock %}
|
{% block modal-form-open %}{% endblock %}
|
||||||
{% if not no_body %}
|
|
||||||
<section class="modal-card-body">
|
<section class="modal-card-body">
|
||||||
{% block modal-body %}{% endblock %}
|
{% block modal-body %}{% endblock %}
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
|
||||||
<footer class="modal-card-foot">
|
<footer class="modal-card-foot">
|
||||||
{% block modal-footer %}{% endblock %}
|
{% block modal-footer %}{% endblock %}
|
||||||
</footer>
|
</footer>
|
||||||
{% block modal-form-close %}{% endblock %}
|
{% block modal-form-close %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
{% if static %}
|
<button
|
||||||
<a href="/" class="modal-close is-large">{{ label }}</a>
|
type="button"
|
||||||
{% else %}
|
class="modal-close is-large"
|
||||||
{% include 'snippets/toggle/toggle_button.html' with label=label class="modal-close is-large" nonbutton=True %}
|
aria-label="{% trans 'Close' %}"
|
||||||
{% endif %}
|
data-modal-close
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,12 @@
|
||||||
|
|
||||||
{% block filter %}
|
{% block filter %}
|
||||||
<label class="label" for="id_sort">{% trans "Order by" %}</label>
|
<label class="label" for="id_sort">{% trans "Order by" %}</label>
|
||||||
<div class="select">
|
<div class="control">
|
||||||
<select name="sort" id="id_sort">
|
<div class="select">
|
||||||
<option value="recent" {% if request.GET.sort == "recent" %}selected{% endif %}>{% trans "Recently active" %}</option>
|
<select name="sort" id="id_sort">
|
||||||
<option value="suggested" {% if request.GET.sort == "suggested" %}selected{% endif %}>{% trans "Suggested" %}</option>
|
<option value="recent" {% if request.GET.sort == "recent" %}selected{% endif %}>{% trans "Recently active" %}</option>
|
||||||
</select>
|
<option value="suggested" {% if request.GET.sort == "suggested" %}selected{% endif %}>{% trans "Suggested" %}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -6,57 +6,22 @@
|
||||||
<h1 class="title">
|
<h1 class="title">
|
||||||
{{ tab.name }}
|
{{ tab.name }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="block is-clipped">
|
|
||||||
<div class="is-pulled-left">
|
|
||||||
<div class="tabs">
|
|
||||||
<ul>
|
|
||||||
{% for stream in streams %}
|
|
||||||
<li class="{% if tab.key == stream.key %}is-active{% endif %}"{% if tab.key == stream.key %} aria-current="page"{% endif %}>
|
|
||||||
<a href="/{{ stream.key }}#feed">{{ stream.shortname }}</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# feed settings #}
|
<div class="tabs">
|
||||||
<details class="detail-pinned-button" {% if settings_saved %}open{% endif %}>
|
<ul>
|
||||||
<summary class="control">
|
{% for stream in streams %}
|
||||||
<span class="button">
|
<li class="{% if tab.key == stream.key %}is-active{% endif %}"{% if tab.key == stream.key %} aria-current="page"{% endif %}>
|
||||||
<span class="icon icon-dots-three m-0-mobile" aria-hidden="true"></span>
|
<a href="/{{ stream.key }}#feed">{{ stream.shortname }}</a>
|
||||||
<span class="is-sr-only-mobile">{{ _("Feed settings") }}</span>
|
</li>
|
||||||
</span>
|
{% endfor %}
|
||||||
</summary>
|
</ul>
|
||||||
<form class="notification level is-align-items-flex-end" method="post" action="/{{ tab.key }}#feed">
|
|
||||||
{% csrf_token %}
|
|
||||||
|
|
||||||
<div class="level-left">
|
|
||||||
<div class="field">
|
|
||||||
<div class="control">
|
|
||||||
<span class="is-flex is-align-items-baseline">
|
|
||||||
<label class="label mt-2 mb-1">Status types</label>
|
|
||||||
{% if settings_saved %}
|
|
||||||
<span class="tag is-success is-light ml-2">{{ _("Saved!") }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
{% for name, value in feed_status_types_options %}
|
|
||||||
<label class="mr-2">
|
|
||||||
<input type="checkbox" name="feed_status_types" value="{{ name }}" {% if name in user.feed_status_types %}checked=""{% endif %}/>
|
|
||||||
{{ value }}
|
|
||||||
</label>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="level-right control">
|
|
||||||
<button class="button is-small is-primary is-outlined" type="submit">
|
|
||||||
{{ _("Save settings") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# feed settings #}
|
||||||
|
{% with "/"|add:tab.key|add:"#feed" as action %}
|
||||||
|
{% include 'feed/feed_filters.html' with size="small" method="post" action=action %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{# 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 href="{{ request.path }}" class="transition-y is-hidden notification is-primary is-block" data-poll-wrapper>
|
||||||
|
@ -64,14 +29,20 @@
|
||||||
{{ allowed_status_types|json_script:"unread-notifications-wrapper" }}
|
{{ allowed_status_types|json_script:"unread-notifications-wrapper" }}
|
||||||
</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' %}
|
||||||
{% now 'Y' as year %}
|
{% now 'Y' as year %}
|
||||||
<section class="block">
|
<section class="block">
|
||||||
{% include 'feed/goal_card.html' with year=year %}
|
{% include 'feed/goal_card.html' with year=year %}
|
||||||
<hr>
|
<hr>
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if annual_summary_year and tab.key == 'home' %}
|
||||||
|
<section class="block is-hidden" data-hide="hide_annual_summary_{{ annual_summary_year }}">
|
||||||
|
{% include 'feed/summary_card.html' with year=annual_summary_year %}
|
||||||
|
<hr>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# activity feed #}
|
{# activity feed #}
|
||||||
|
|
5
bookwyrm/templates/feed/feed_filters.html
Normal file
5
bookwyrm/templates/feed/feed_filters.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
||||||
|
|
||||||
|
{% block filter_fields %}
|
||||||
|
{% include 'feed/status_types_filter.html' %}
|
||||||
|
{% endblock %}
|
|
@ -8,82 +8,7 @@
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<div class="column is-one-third">
|
<div class="column is-one-third">
|
||||||
<section class="block">
|
{% include "feed/suggested_books.html" %}
|
||||||
<h2 class="title is-4">{% trans "Your Books" %}</h2>
|
|
||||||
{% if not suggested_books %}
|
|
||||||
<p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p>
|
|
||||||
{% else %}
|
|
||||||
{% with active_book=request.GET.book %}
|
|
||||||
<div class="tab-group">
|
|
||||||
<div class="tabs is-small">
|
|
||||||
<ul role="tablist">
|
|
||||||
{% for shelf in suggested_books %}
|
|
||||||
{% if shelf.books %}
|
|
||||||
{% with shelf_counter=forloop.counter %}
|
|
||||||
<li>
|
|
||||||
<p>
|
|
||||||
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
|
|
||||||
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
|
|
||||||
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
|
|
||||||
{% else %}{{ shelf.name }}{% endif %}
|
|
||||||
</p>
|
|
||||||
<div class="tabs is-small is-toggle">
|
|
||||||
<ul>
|
|
||||||
{% for book in shelf.books %}
|
|
||||||
<li class="{% if active_book == book.id|stringformat:'d' %}is-active{% elif not active_book and shelf_counter == 1 and forloop.first %}is-active{% endif %}">
|
|
||||||
<a
|
|
||||||
href="{{ request.path }}?book={{ book.id }}"
|
|
||||||
id="tab_book_{{ book.id }}"
|
|
||||||
role="tab"
|
|
||||||
aria-label="{{ book.title }}"
|
|
||||||
aria-selected="{% if active_book == book.id|stringformat:'d' %}true{% elif shelf_counter == 1 and forloop.first %}true{% else %}false{% endif %}"
|
|
||||||
aria-controls="book_{{ book.id }}">
|
|
||||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% for shelf in suggested_books %}
|
|
||||||
{% with shelf_counter=forloop.counter %}
|
|
||||||
{% for book in shelf.books %}
|
|
||||||
<div
|
|
||||||
class="suggested-tabs card"
|
|
||||||
role="tabpanel"
|
|
||||||
id="book_{{ book.id }}"
|
|
||||||
{% if active_book and active_book == book.id|stringformat:'d' %}{% elif not active_book and shelf_counter == 1 and forloop.first %}{% else %} hidden{% endif %}
|
|
||||||
aria-labelledby="tab_book_{{ book.id }}">
|
|
||||||
|
|
||||||
<div class="card-header">
|
|
||||||
<div class="card-header-title">
|
|
||||||
<div>
|
|
||||||
<p class="mb-2">{% include 'snippets/book_titleby.html' with book=book %}</p>
|
|
||||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-header-icon is-hidden-tablet">
|
|
||||||
{% trans "Close" as button_text %}
|
|
||||||
{% include 'snippets/toggle/toggle_button.html' with label=button_text controls_text="book" controls_uid=book.id class="delete" nonbutton=True pressed=True %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-content">
|
|
||||||
{% include 'snippets/create_status.html' with book=book %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{% if goal %}
|
{% if goal %}
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<div class="block">
|
<div class="block">
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="is-main block" id="anchor-{{ status.id }}">
|
<div class="is-main block">
|
||||||
{% include 'snippets/status/status.html' with status=status main=True %}
|
{% include 'snippets/status/status.html' with status=status main=True %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
16
bookwyrm/templates/feed/status_types_filter.html
Normal file
16
bookwyrm/templates/feed/status_types_filter.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends 'snippets/filters_panel/filter_field.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block filter %}
|
||||||
|
<label class="label mt-2 mb-1">Status types</label>
|
||||||
|
|
||||||
|
<div class="is-flex is-flex-direction-row is-flex-direction-column-mobile">
|
||||||
|
{% for name, value in feed_status_types_options %}
|
||||||
|
<label class="mr-2">
|
||||||
|
<input type="checkbox" name="feed_status_types" value="{{ name }}" {% if name in user.feed_status_types %}checked=""{% endif %}/>
|
||||||
|
{{ value }}
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
79
bookwyrm/templates/feed/suggested_books.html
Normal file
79
bookwyrm/templates/feed/suggested_books.html
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bookwyrm_tags %}
|
||||||
|
|
||||||
|
{% suggested_books as suggested_books %}
|
||||||
|
<section class="block">
|
||||||
|
<h2 class="title is-4">{% trans "Your Books" %}</h2>
|
||||||
|
{% if not suggested_books %}
|
||||||
|
<p>{% trans "There are no books here right now! Try searching for a book to get started" %}</p>
|
||||||
|
{% else %}
|
||||||
|
{% with active_book=request.GET.book %}
|
||||||
|
<div class="tab-group">
|
||||||
|
<div class="tabs is-small">
|
||||||
|
<ul role="tablist">
|
||||||
|
{% for shelf in suggested_books %}
|
||||||
|
{% if shelf.books %}
|
||||||
|
{% with shelf_counter=forloop.counter %}
|
||||||
|
<li>
|
||||||
|
<p>
|
||||||
|
{% if shelf.identifier == 'to-read' %}{% trans "To Read" %}
|
||||||
|
{% elif shelf.identifier == 'reading' %}{% trans "Currently Reading" %}
|
||||||
|
{% elif shelf.identifier == 'read' %}{% trans "Read" %}
|
||||||
|
{% else %}{{ shelf.name }}{% endif %}
|
||||||
|
</p>
|
||||||
|
<div class="tabs is-small is-toggle">
|
||||||
|
<ul>
|
||||||
|
{% for book in shelf.books %}
|
||||||
|
<li class="{% if active_book == book.id|stringformat:'d' %}is-active{% elif not active_book and shelf_counter == 1 and forloop.first %}is-active{% endif %}">
|
||||||
|
<a
|
||||||
|
href="{{ request.path }}?book={{ book.id }}"
|
||||||
|
id="tab_book_{{ book.id }}"
|
||||||
|
role="tab"
|
||||||
|
aria-label="{{ book.title }}"
|
||||||
|
aria-selected="{% if active_book == book.id|stringformat:'d' %}true{% elif shelf_counter == 1 and forloop.first %}true{% else %}false{% endif %}"
|
||||||
|
aria-controls="book_{{ book.id }}">
|
||||||
|
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% for shelf in suggested_books %}
|
||||||
|
{% with shelf_counter=forloop.counter %}
|
||||||
|
{% for book in shelf.books %}
|
||||||
|
<div
|
||||||
|
class="suggested-tabs card"
|
||||||
|
role="tabpanel"
|
||||||
|
id="book_{{ book.id }}"
|
||||||
|
{% if active_book and active_book == book.id|stringformat:'d' %}{% elif not active_book and shelf_counter == 1 and forloop.first %}{% else %} hidden{% endif %}
|
||||||
|
aria-labelledby="tab_book_{{ book.id }}">
|
||||||
|
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-header-title">
|
||||||
|
<div>
|
||||||
|
<p class="mb-2">{% include 'snippets/book_titleby.html' with book=book %}</p>
|
||||||
|
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-header-icon is-hidden-tablet">
|
||||||
|
{% trans "Close" as button_text %}
|
||||||
|
{% include 'snippets/toggle/toggle_button.html' with label=button_text controls_text="book" controls_uid=book.id class="delete" nonbutton=True pressed=True %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
{% include 'snippets/create_status.html' with book=book %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
31
bookwyrm/templates/feed/summary_card.html
Normal file
31
bookwyrm/templates/feed/summary_card.html
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
<header class="card-header has-background-success-dark">
|
||||||
|
<h3 class="card-header-title has-text-white">
|
||||||
|
<span class="icon is-size-3 mr-2" aria-hidden="true">📚</span>
|
||||||
|
<span class="icon is-size-3 mr-2" aria-hidden="true">✨</span>
|
||||||
|
{% blocktrans %}{{ year }} in the books{% endblocktrans %}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="card-header-icon has-text-white">
|
||||||
|
{% trans "Dismiss message" as button_text %}
|
||||||
|
<button class="delete set-display" type="button" data-id="hide_annual_summary_{{ year }}" data-value="true">
|
||||||
|
<span>{% trans "Dismiss message" %}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="card-content">
|
||||||
|
<p class="mb-3">
|
||||||
|
{% blocktrans %}The end of the year is the best moment to take stock of all the books read during the last 12 months. How many pages have you read? Which book is your best-rated of the year? We compiled these stats, and more!{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'annual-summary' request.user.localname year %}" class="button is-success has-background-success-dark">
|
||||||
|
{% blocktrans %}Discover your stats for {{ year }}!{% endblocktrans %}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</article>
|
|
@ -10,7 +10,12 @@
|
||||||
<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">
|
||||||
<img class="image logo mr-2" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" aria-hidden="true">
|
<img
|
||||||
|
class="image logo mr-2"
|
||||||
|
src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}"
|
||||||
|
aria-hidden="true"
|
||||||
|
alt="{{ site.name }}"
|
||||||
|
>
|
||||||
<h1 class="modal-card-title" id="get_started_header">
|
<h1 class="modal-card-title" id="get_started_header">
|
||||||
{% blocktrans %}Welcome to {{ site_name }}!{% endblocktrans %}
|
{% blocktrans %}Welcome to {{ site_name }}!{% endblocktrans %}
|
||||||
<span class="subtitle is-block">
|
<span class="subtitle is-block">
|
||||||
|
|
|
@ -13,13 +13,26 @@
|
||||||
<div class="column is-two-thirds">
|
<div class="column is-two-thirds">
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<label class="label" for="id_name">{% trans "Display name:" %}</label>
|
<label class="label" for="id_name">{% trans "Display name:" %}</label>
|
||||||
<input type="text" name="name" maxlength="100" class="input" id="id_name" placeholder="{{ user.localname }}" value="{% if request.user.name %}{{ request.user.name }}{% endif %}">
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
maxlength="100"
|
||||||
|
class="input"
|
||||||
|
id="id_name"
|
||||||
|
placeholder="{{ user.localname }}"
|
||||||
|
value="{% if request.user.name %}{{ request.user.name }}{% endif %}"
|
||||||
|
>
|
||||||
|
|
||||||
{% include 'snippets/form_errors.html' with errors_list=form.name.errors id="desc_name" %}
|
{% include 'snippets/form_errors.html' with errors_list=form.name.errors id="desc_name" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
|
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
|
||||||
<textarea name="summary" cols="None" rows="None" class="textarea" id="id_summary" placeholder="{% trans 'A little bit about you' %}">{% if request.user.summary %}{{ request.user.summary }}{% endif %}</textarea>
|
<textarea
|
||||||
|
name="summary"
|
||||||
|
class="textarea"
|
||||||
|
id="id_summary"
|
||||||
|
placeholder="{% trans 'A little bit about you' %}"
|
||||||
|
>{% if request.user.summary %}{{ request.user.summary }}{% endif %}</textarea>
|
||||||
|
|
||||||
{% include 'snippets/form_errors.html' with errors_list=form.summary.errors id="desc_summary" %}
|
{% include 'snippets/form_errors.html' with errors_list=form.summary.errors id="desc_summary" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,8 +14,7 @@
|
||||||
<button class="button is-danger" type="submit">
|
<button class="button is-danger" type="submit">
|
||||||
{% trans "Delete" %}
|
{% trans "Delete" %}
|
||||||
</button>
|
</button>
|
||||||
{% trans "Cancel" as button_text %}
|
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="delete_group" controls_uid=group.id %}
|
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -9,5 +9,5 @@
|
||||||
<form name="edit-group" method="post" action="{% url 'group' group.id %}">
|
<form name="edit-group" method="post" action="{% url 'group' group.id %}">
|
||||||
{% include 'groups/form.html' %}
|
{% include 'groups/form.html' %}
|
||||||
</form>
|
</form>
|
||||||
{% include "groups/delete_group_modal.html" with controls_text="delete_group" controls_uid=group.id %}
|
{% include "groups/delete_group_modal.html" with id="delete_group" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -2,8 +2,5 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block searchresults %}
|
{% block searchresults %}
|
||||||
<h2 class="title is-5">
|
|
||||||
{% trans "Add new members!" %}
|
|
||||||
</h2>
|
|
||||||
{% include 'groups/suggested_users.html' with suggested_users=suggested_users %}
|
{% include 'groups/suggested_users.html' with suggested_users=suggested_users %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -5,30 +5,29 @@
|
||||||
<div class="column is-two-thirds">
|
<div class="column is-two-thirds">
|
||||||
<input type="hidden" name="user" value="{{ request.user.id }}" />
|
<input type="hidden" name="user" value="{{ request.user.id }}" />
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_name">{% trans "Group Name:" %}</label>
|
<label class="label" for="group_form_id_name">{% trans "Group Name:" %}</label>
|
||||||
{{ group_form.name }}
|
{{ group_form.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="id_description">{% trans "Group Description:" %}</label>
|
<label class="label" for="group_form_id_description">{% trans "Group Description:" %}</label>
|
||||||
{{ group_form.description }}
|
{{ group_form.description }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns is-mobile">
|
<div class="is-flex">
|
||||||
<div class="column">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control">
|
|
||||||
{% include 'snippets/privacy_select_no_followers.html' with current=group.privacy %}
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if group.id %}
|
{% if group.id %}
|
||||||
<div class="column is-narrow">
|
<div class="is-flex-grow-1">
|
||||||
{% trans "Delete group" as button_text %}
|
<button type="button" data-modal-open="delete_group" class="button is-danger">
|
||||||
{% include 'snippets/toggle/toggle_button.html' with class="is-danger" text=button_text icon_with_text="x" controls_text="delete_group" controls_uid=group.id focus="modal_title_delete_group" %}
|
{% trans "Delete group" %}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div class="control">
|
||||||
|
{% include 'snippets/privacy_select_no_followers.html' with current=group.privacy %}
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,37 +1,41 @@
|
||||||
{% extends 'groups/layout.html' %}
|
{% extends 'groups/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
|
{% load bookwyrm_group_tags %}
|
||||||
{% load markdown %}
|
{% load markdown %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
<div class="columns mt-3">
|
<div class="columns mt-3">
|
||||||
<section class="column is-three-quarters">
|
<section class="column is-three-quarters">
|
||||||
|
|
||||||
{% if group.user == request.user %}
|
|
||||||
<div class="block">
|
|
||||||
<form class="field has-addons" method="get" action="{% url 'group-find-users' group.id %}">
|
|
||||||
<div class="control">
|
|
||||||
<input type="text" name="user_query" value="{{ request.GET.user_query }}" class="input" placeholder="{% trans 'Search to add a user' %}" aria-label="{% trans 'Search to add a user' %}">
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button class="button" type="submit">
|
|
||||||
<span class="icon icon-search" title="{% trans 'Search' %}">
|
|
||||||
<span class="is-sr-only">{% trans "Search" %}</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% block searchresults %}
|
{% block searchresults %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
{% include "groups/members.html" with group=group %}
|
{% include "groups/members.html" with group=group %}
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
<h2 class="title is-5">Lists</h2>
|
</div>
|
||||||
|
<header class="columns content is-mobile">
|
||||||
|
<div class="column">
|
||||||
|
<h2 class="title is-5">{% trans "Lists" %}</h2>
|
||||||
|
<p class="subtitle is-6">
|
||||||
|
{% trans "Members of this group can create group-curated lists." %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% if request.user.is_authenticated and group|is_member:request.user %}
|
||||||
|
<div class="column is-narrow is-flex">
|
||||||
|
{% trans "Create List" as button_text %}
|
||||||
|
{% include 'snippets/toggle/open_button.html' with controls_text="create_list" icon_with_text="plus" text=button_text focus="create_list_header" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</header>
|
||||||
|
{% if request.user.is_authenticated and group|is_member:request.user %}
|
||||||
|
<div class="block">
|
||||||
|
{% include 'lists/create_form.html' with controls_text="create_list" curation_group=group %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="columns mt-3">
|
||||||
|
<section class="column is-three-quarters">
|
||||||
{% if not lists %}
|
{% if not lists %}
|
||||||
<p>{% trans "This group has no lists" %}</p>
|
<p>{% trans "This group has no lists" %}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -45,19 +49,17 @@
|
||||||
<a href="{{ list.local_path }}">{{ list.name }}</a> <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>
|
<a href="{{ list.local_path }}">{{ list.name }}</a> <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>
|
||||||
</h4>
|
</h4>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{% with list_books=list.listitem_set.all|slice:5 %}
|
{% with list_books=list.listitem_set.all|slice:5 %}
|
||||||
{% if list_books %}
|
{% if list_books %}
|
||||||
<div class="card-image columns is-mobile is-gapless is-clipped">
|
<div class="card-image columns is-mobile is-gapless is-clipped">
|
||||||
{% for book in list_books %}
|
{% for book in list_books %}
|
||||||
<a class="column is-cover" href="{{ book.book.local_path }}">
|
<a class="column is-cover" href="{{ book.book.local_path }}">
|
||||||
{% include 'snippets/book_cover.html' with book=book.book cover_class='is-h-s' size='small' aria='show' %}
|
{% include 'snippets/book_cover.html' with book=book.book cover_class='is-h-s' size='small' aria='show' %}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<div class="card-content is-flex-grow-0">
|
<div class="card-content is-flex-grow-0">
|
||||||
<div class="is-clipped" {% if list.description %}title="{{ list.description }}"{% endif %}>
|
<div class="is-clipped" {% if list.description %}title="{{ list.description }}"{% endif %}>
|
||||||
{% if list.description %}
|
{% if list.description %}
|
||||||
|
@ -74,9 +76,8 @@
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "snippets/pagination.html" with page=items %}
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load bookwyrm_group_tags %}
|
||||||
|
|
||||||
{% block title %}{{ group.name }}{% endblock %}
|
{% block title %}{{ group.name }}{% endblock %}
|
||||||
|
|
||||||
|
@ -11,12 +12,12 @@
|
||||||
{% include 'groups/created_text.html' with group=group %}
|
{% include 'groups/created_text.html' with group=group %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% if request.user == group.user %}
|
||||||
<div class="column is-narrow is-flex">
|
<div class="column is-narrow is-flex">
|
||||||
{% if request.user == group.user %}
|
{% trans "Edit group" as button_text %}
|
||||||
{% trans "Edit group" as button_text %}
|
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit_group" focus="edit_group_header" %}
|
||||||
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit_group" focus="edit_group_header" %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="block content">
|
<div class="block content">
|
||||||
|
|
|
@ -5,8 +5,22 @@
|
||||||
{% load bookwyrm_group_tags %}
|
{% load bookwyrm_group_tags %}
|
||||||
|
|
||||||
<h2 class="title is-5">Group Members</h2>
|
<h2 class="title is-5">Group Members</h2>
|
||||||
<p class="subtitle is-6">{% trans "Members can add and remove books on a group's book lists" %}</p>
|
{% if group.user == request.user %}
|
||||||
|
<div class="block">
|
||||||
|
<form class="field has-addons" method="get" action="{% url 'group-find-users' group.id %}">
|
||||||
|
<div class="control">
|
||||||
|
<input type="text" name="user_query" value="{{ request.GET.user_query }}" class="input" placeholder="{% trans 'Search to add a user' %}" aria-label="{% trans 'Search to add a user' %}">
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<button class="button" type="submit">
|
||||||
|
<span class="icon icon-search" title="{% trans 'Search' %}">
|
||||||
|
<span class="is-sr-only">{% trans "Search" %}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if group.user != request.user and group|is_member:request.user %}
|
{% if group.user != request.user and group|is_member:request.user %}
|
||||||
<form action="{% url 'remove-group-member' %}" method="POST" class="my-4">
|
<form action="{% url 'remove-group-member' %}" method="POST" class="my-4">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
{% if suggested_users %}
|
{% if suggested_users %}
|
||||||
|
<h2 class="title is-5">
|
||||||
|
{% trans "Add new members!" %}
|
||||||
|
</h2>
|
||||||
<div class="column is-flex is-flex-grow-0">
|
<div class="column is-flex is-flex-grow-0">
|
||||||
{% for user in suggested_users %}
|
{% for user in suggested_users %}
|
||||||
<div class="box has-text-centered is-shadowless has-background-white-bis m-2">
|
<div class="box has-text-centered is-shadowless has-background-white-bis m-2">
|
||||||
|
|
|
@ -234,7 +234,3 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endspaceless %}{% endblock %}
|
{% endspaceless %}{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script src="{% static "js/check_all.js" %}?v={{ js_cache }}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
{% extends 'landing/layout.html' %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block panel %}
|
|
||||||
<div class="block columns mt-4">
|
|
||||||
<nav class="menu column is-one-quarter">
|
|
||||||
<h2 class="menu-label">{% blocktrans with site_name=site.name %}About {{ site_name }}{% endblocktrans %}</h2>
|
|
||||||
<ul class="menu-list">
|
|
||||||
<li>
|
|
||||||
<a href="#coc">{% trans "Code of Conduct" %}</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#privacy">{% trans "Privacy Policy" %}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="column content">
|
|
||||||
<div class="block" id="coc">
|
|
||||||
<h2 class="title">{% trans "Code of Conduct" %}</h2>
|
|
||||||
<div class="content">
|
|
||||||
{{ site.code_of_conduct | safe }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr aria-hidden="true">
|
|
||||||
|
|
||||||
<div class="block" id="privacy">
|
|
||||||
<h2 class="title">{% trans "Privacy Policy" %}</h2>
|
|
||||||
<div class="content">
|
|
||||||
{{ site.privacy_policy | safe }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,11 +1,13 @@
|
||||||
{% extends 'landing/layout.html' %}
|
{% extends 'landing/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load cache %}
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
<div class="block is-hidden-tablet">
|
<div class="block is-hidden-tablet">
|
||||||
<h2 class="title has-text-centered">{% trans "Recent Books" %}</h2>
|
<h2 class="title has-text-centered">{% trans "Recent Books" %}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% cache 60 * 60 %}
|
||||||
<section class="tile is-ancestor">
|
<section class="tile is-ancestor">
|
||||||
<div class="tile is-vertical is-6">
|
<div class="tile is-vertical is-6">
|
||||||
<div class="tile is-parent">
|
<div class="tile is-parent">
|
||||||
|
@ -46,5 +48,5 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{% endcache %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{% block about_panel %}
|
||||||
<section class="tile is-ancestor">
|
<section class="tile is-ancestor">
|
||||||
<div class="tile is-7 is-parent">
|
<div class="tile is-7 is-parent">
|
||||||
<div class="tile is-child box">
|
<div class="tile is-child box">
|
||||||
|
@ -89,6 +90,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block panel %}{% endblock %}
|
{% block panel %}{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
{% include 'snippets/opengraph_images.html' %}
|
{% include 'snippets/opengraph_images.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<meta name="twitter:image:alt" content="BookWyrm Logo">
|
<meta name="twitter:image:alt" content="BookWyrm Logo">
|
||||||
|
|
||||||
|
{% block head_links %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar" aria-label="main navigation">
|
<nav class="navbar" aria-label="main navigation">
|
||||||
|
@ -36,7 +38,7 @@
|
||||||
<a class="navbar-item" href="/">
|
<a class="navbar-item" href="/">
|
||||||
<img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="{% blocktrans with site_name=site.name %}{{ site_name }} home page{% endblocktrans %}">
|
<img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="{% blocktrans with site_name=site.name %}{{ site_name }} home page{% endblocktrans %}">
|
||||||
</a>
|
</a>
|
||||||
<form class="navbar-item column" action="{% url 'search' %}">
|
<form class="navbar-item column is-align-items-start pt-5" action="{% url 'search' %}">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
|
@ -56,25 +58,22 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div role="button" tabindex="0" class="navbar-burger pulldown-menu" data-controls="main_nav" aria-expanded="false">
|
<button type="button" tabindex="0" class="navbar-burger pulldown-menu my-4" data-controls="main_nav" aria-expanded="false">
|
||||||
<div class="navbar-item mt-3">
|
<i class="icon icon-dots-three-vertical" aria-hidden="true"></i>
|
||||||
<div class="icon icon-dots-three-vertical" title="{% trans 'Main navigation menu' %}">
|
<span class="is-sr-only">{% trans "Main navigation menu" %}</span>
|
||||||
<span class="is-sr-only">{% trans "Main navigation menu" %}</span>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-menu" id="main_nav">
|
<div class="navbar-menu" id="main_nav">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<a href="/#feed" class="navbar-item">
|
<a href="/#feed" class="navbar-item mt-3 py-0">
|
||||||
{% trans "Feed" %}
|
{% trans "Feed" %}
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'lists' %}" class="navbar-item">
|
<a href="{% url 'lists' %}" class="navbar-item mt-3 py-0">
|
||||||
{% trans "Lists" %}
|
{% trans "Lists" %}
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'discover' %}" class="navbar-item">
|
<a href="{% url 'discover' %}" class="navbar-item mt-3 py-0">
|
||||||
{% trans "Discover" %}
|
{% trans "Discover" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -82,7 +81,7 @@
|
||||||
|
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item mt-3 py-0 has-dropdown is-hoverable">
|
||||||
<a
|
<a
|
||||||
href="{{ request.user.local_path }}"
|
href="{{ request.user.local_path }}"
|
||||||
class="navbar-link pulldown-menu"
|
class="navbar-link pulldown-menu"
|
||||||
|
@ -141,7 +140,7 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-item">
|
<div class="navbar-item mt-3 py-0">
|
||||||
<a href="{% url 'notifications' %}" class="tags has-addons">
|
<a href="{% url 'notifications' %}" class="tags has-addons">
|
||||||
<span class="tag is-medium">
|
<span class="tag is-medium">
|
||||||
<span class="icon icon-bell" title="{% trans 'Notifications' %}">
|
<span class="icon icon-bell" title="{% trans 'Notifications' %}">
|
||||||
|
@ -159,7 +158,7 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="navbar-item">
|
<div class="navbar-item pt-5 pb-0">
|
||||||
{% if request.path != '/login' and request.path != '/login/' %}
|
{% if request.path != '/login' and request.path != '/login/' %}
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
|
|
|
@ -7,6 +7,6 @@
|
||||||
|
|
||||||
{% block form %}
|
{% block form %}
|
||||||
<form name="create-list" method="post" action="{% url 'lists' %}">
|
<form name="create-list" method="post" action="{% url 'lists' %}">
|
||||||
{% include 'lists/form.html' %}
|
{% include 'lists/form.html' with curation_group=group %}
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,72 +1,74 @@
|
||||||
{% extends 'lists/layout.html' %}
|
{% extends 'lists/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||||
|
<ul>
|
||||||
|
<li><a href="{% url 'lists' %}">{% trans "Lists" %}</a></li>
|
||||||
|
<li><a href="{% url 'list' list.id %}">{{ list.name|truncatechars:30 }}</a></li>
|
||||||
|
<li class="is-active">
|
||||||
|
<a href="#" aria-current="page">
|
||||||
|
{% trans "Curate" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<div class="columns is-mobile is-multiline is-align-items-baseline">
|
<h2 class="title is-4">{% trans "Pending Books" %}</h2>
|
||||||
<div class="column is-narrow">
|
|
||||||
<h2 class="title is-4">{% trans "Pending Books" %}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="column is-narrow"><a href="{% url 'list' list.id %}">{% trans "Go to list" %}</a></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if not pending.exists %}
|
{% if not pending.exists %}
|
||||||
<p>{% trans "You're all set!" %}</p>
|
<p><em>{% trans "You're all set!" %}</em></p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<dl>
|
<div class="columns">
|
||||||
{% for item in pending %}
|
{% for item in pending %}
|
||||||
{% with book=item.book %}
|
{% with book=item.book %}
|
||||||
<div
|
<div class="column">
|
||||||
class="
|
<div class="columns is-mobile">
|
||||||
columns is-gapless
|
<a
|
||||||
is-vcentered is-justify-content-space-between
|
class="column is-cover"
|
||||||
mb-6
|
href="{{ book.local_path }}"
|
||||||
"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<dt class="column mr-auto">
|
{% include 'snippets/book_cover.html' with cover_class='is-w-xs-mobile is-w-s is-h-xs-mobile is-h-s' size_mobile='xsmall' size='small' %}
|
||||||
<div class="columns is-mobile is-gapless is-vcentered">
|
</a>
|
||||||
<a
|
|
||||||
class="column is-cover"
|
<div class="column ml-3">
|
||||||
href="{{ book.local_path }}"
|
{% include 'snippets/book_titleby.html' %}
|
||||||
aria-hidden="true"
|
|
||||||
>
|
<p>
|
||||||
{% include 'snippets/book_cover.html' with cover_class='is-w-xs-mobile is-w-s is-h-xs-mobile is-h-s' size_mobile='xsmall' size='small' %}
|
{% trans "Suggested by" %}
|
||||||
|
|
||||||
|
<a href="{{ item.user.local_path }}">
|
||||||
|
{{ item.user.display_name }}
|
||||||
</a>
|
</a>
|
||||||
|
</p>
|
||||||
<div class="column ml-3">
|
</div>
|
||||||
{% include 'snippets/book_titleby.html' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</dt>
|
|
||||||
|
|
||||||
<dd class="column is-4-tablet mx-3-tablet my-3-mobile">
|
|
||||||
{% trans "Suggested by" %}
|
|
||||||
|
|
||||||
<a href="{{ item.user.local_path }}">
|
|
||||||
{{ item.user.display_name }}
|
|
||||||
</a>
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
<dd class="column is-narrow field has-addons">
|
|
||||||
<form class="control" method="POST" action="{% url 'list-curate' list.id %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="item" value="{{ item.id }}">
|
|
||||||
<input type="hidden" name="approved" value="true">
|
|
||||||
<button class="button">{% trans "Approve" %}</button>
|
|
||||||
</form>
|
|
||||||
<form class="control" method="POST" action="{% url 'list-curate' list.id %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="item" value="{{ item.id }}">
|
|
||||||
<input type="hidden" name="approved" value="false">
|
|
||||||
<button class="button is-danger is-light">{% trans "Discard" %}</button>
|
|
||||||
</form>
|
|
||||||
</dd>
|
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<div class="field has-addons">
|
||||||
|
<form class="control" method="POST" action="{% url 'list-curate' list.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="item" value="{{ item.id }}">
|
||||||
|
<input type="hidden" name="approved" value="true">
|
||||||
|
<button type="submit" class="button">{% trans "Approve" %}</button>
|
||||||
|
</form>
|
||||||
|
<form class="control" method="POST" action="{% url 'list-curate' list.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="item" value="{{ item.id }}">
|
||||||
|
<input type="hidden" name="approved" value="false">
|
||||||
|
<button type="submit" class="button is-danger is-light">{% trans "Discard" %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</dl>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -14,8 +14,9 @@
|
||||||
<button class="button is-danger" type="submit">
|
<button class="button is-danger" type="submit">
|
||||||
{% trans "Delete" %}
|
{% trans "Delete" %}
|
||||||
</button>
|
</button>
|
||||||
{% trans "Cancel" as button_text %}
|
<button type="button" class="button" data-modal-close>
|
||||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="delete_list" controls_uid=list.id %}
|
{% trans "Cancel" %}
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -9,5 +9,5 @@
|
||||||
<form name="edit-list" method="post" action="{% url 'list' list.id %}">
|
<form name="edit-list" method="post" action="{% url 'list' list.id %}">
|
||||||
{% include 'lists/form.html' %}
|
{% include 'lists/form.html' %}
|
||||||
</form>
|
</form>
|
||||||
{% include "lists/delete_list_modal.html" with controls_text="delete_list" controls_uid=list.id %}
|
{% include "lists/delete_list_modal.html" with id="delete_list" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -18,33 +18,82 @@
|
||||||
<fieldset class="field">
|
<fieldset class="field">
|
||||||
<legend class="label">{% trans "List curation:" %}</legend>
|
<legend class="label">{% trans "List curation:" %}</legend>
|
||||||
|
|
||||||
<label class="field" data-hides="list_group_selector">
|
<div class="field" data-hides="list_group_selector">
|
||||||
<input type="radio" name="curation" value="closed"{% if not list or list.curation == 'closed' %} checked{% endif %}> {% trans "Closed" %}
|
<input
|
||||||
<p class="help mb-2">{% trans "Only you can add and remove books to this list" %}</p>
|
type="radio"
|
||||||
</label>
|
name="curation"
|
||||||
|
value="closed"
|
||||||
|
aria-described-by="id_curation_closed_help"
|
||||||
|
id="id_curation_closed"
|
||||||
|
{% if not curation_group.exists or not list or list.curation == 'closed' %}checked{% endif %}
|
||||||
|
>
|
||||||
|
<label for="id_curation_closed">
|
||||||
|
{% trans "Closed" %}
|
||||||
|
</label>
|
||||||
|
<p class="help mb-2" id="id_curation_closed_help">
|
||||||
|
{% trans "Only you can add and remove books to this list" %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="field" data-hides="list_group_selector">
|
<div class="field" data-hides="list_group_selector">
|
||||||
<input type="radio" name="curation" value="curated"{% if list.curation == 'curated' %} checked{% endif %}> {% trans "Curated" %}
|
<input
|
||||||
<p class="help mb-2">{% trans "Anyone can suggest books, subject to your approval" %}</p>
|
type="radio"
|
||||||
</label>
|
name="curation"
|
||||||
|
value="curated"
|
||||||
|
aria-described-by="id_curation_curated_help"
|
||||||
|
id="id_curation_curated"
|
||||||
|
{% if list.curation == 'curated' %} checked{% endif %}
|
||||||
|
>
|
||||||
|
<label for="id_curation_curated">
|
||||||
|
{% trans "Curated" %}
|
||||||
|
</label>
|
||||||
|
<p class="help mb-2" id="id_curation_curated_help">
|
||||||
|
{% trans "Anyone can suggest books, subject to your approval" %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="field" data-hides="list_group_selector">
|
<div class="field" data-hides="list_group_selector">
|
||||||
<input type="radio" name="curation" value="open"{% if list.curation == 'open' %} checked{% endif %}> {% trans "Open" context "curation type" %}
|
<input
|
||||||
<p class="help mb-2">{% trans "Anyone can add books to this list" %}</p>
|
type="radio"
|
||||||
</label>
|
name="curation"
|
||||||
|
value="open"
|
||||||
|
aria-described-by="id_curation_open_help"
|
||||||
|
id="id_curation_open"
|
||||||
|
{% if list.curation == 'open' %} checked{% endif %}
|
||||||
|
>
|
||||||
|
<label for="id_curation_open">
|
||||||
|
{% trans "Open" context "curation type" %}
|
||||||
|
</label>
|
||||||
|
<p class="help mb-2" id="id_curation_open_help">
|
||||||
|
{% trans "Anyone can add books to this list" %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="field hidden-form">
|
<div class="field hidden-form">
|
||||||
<input type="radio" name="curation" value="group"{% if list.curation == 'group' %} checked{% endif %} > {% trans "Group" %}
|
<input
|
||||||
<p class="help mb-2">{% trans "Group members can add to and remove from this list" %}</p>
|
type="radio"
|
||||||
<fieldset class="{% if list.curation != 'group' %}is-hidden{% endif %}" id="list_group_selector">
|
name="curation"
|
||||||
|
value="group"
|
||||||
|
aria-described-by="id_curation_group_help"
|
||||||
|
id="id_curation_group"
|
||||||
|
{% if curation_group.id or list.curation == 'group' %}checked{% endif %}
|
||||||
|
>
|
||||||
|
<label for="id_curation_group">
|
||||||
|
{% trans "Group" %}
|
||||||
|
</label>
|
||||||
|
<p class="help mb-2" id="id_curation_group_help">
|
||||||
|
{% trans "Group members can add to and remove from this list" %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<fieldset class="{% if list.curation != 'group' and not curation_group %}is-hidden{% endif %}" id="list_group_selector">
|
||||||
{% if user.memberships.exists %}
|
{% if user.memberships.exists %}
|
||||||
<label class="label" for="id_group" id="group">{% trans "Select Group" %}</label>
|
<label class="label" for="id_group" id="group">{% trans "Select Group" %}</label>
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="select control">
|
<div class="select control">
|
||||||
<select name="group" id="id_group">
|
<select name="group" id="id_group">
|
||||||
<option value="" disabled {% if not list.group %} selected{% endif %}>{% trans "Select a group" %}</option>
|
<option value="" disabled {% if not list.group and not curation_group %} selected{% endif %}>{% trans "Select a group" %}</option>
|
||||||
{% for membership in user.memberships.all %}
|
{% for membership in user.memberships.all %}
|
||||||
<option value="{{ membership.group.id }}" {% if list.group.id == membership.group.id %} selected{% endif %}>{{ membership.group.name }}</option>
|
<option value="{{ membership.group.id }}" {% if list.group.id == membership.group.id or curation_group.id == membership.group.id %} selected{% endif %}>{{ membership.group.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,25 +110,24 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</label>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns is-mobile">
|
<div class="is-flex">
|
||||||
<div class="column">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control">
|
|
||||||
{% include 'snippets/privacy_select.html' with current=list.privacy %}
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if list.id %}
|
{% if list.id %}
|
||||||
<div class="column is-narrow">
|
<div class="is-flex-grow-1">
|
||||||
{% trans "Delete list" as button_text %}
|
<button type="button" data-modal-open="delete_list" class="button is-danger">
|
||||||
{% include 'snippets/toggle/toggle_button.html' with class="is-danger" text=button_text icon_with_text="x" controls_text="delete_list" controls_uid=list.id focus="modal_title_delete_list" %}
|
{% trans "Delete list" %}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div class="control">
|
||||||
|
{% include 'snippets/privacy_select.html' with current=list.privacy %}
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
{% include 'lists/created_text.html' with list=list %}
|
{% include 'lists/created_text.html' with list=list %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-narrow is-flex">
|
<div class="column is-narrow is-flex">
|
||||||
{% if request.user == list.user %}
|
{% if request.user == list.user %}
|
||||||
{% trans "Edit List" as button_text %}
|
{% trans "Edit List" as button_text %}
|
||||||
|
@ -20,6 +21,8 @@
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{% block breadcrumbs %}{% endblock %}
|
||||||
|
|
||||||
<div class="block content">
|
<div class="block content">
|
||||||
{% include 'snippets/trimmed_text.html' with full=list.description %}
|
{% include 'snippets/trimmed_text.html' with full=list.description %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,6 +4,19 @@
|
||||||
{% load bookwyrm_group_tags %}
|
{% load bookwyrm_group_tags %}
|
||||||
{% load markdown %}
|
{% load markdown %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||||
|
<ul>
|
||||||
|
<li><a href="{% url 'lists' %}">{% trans "Lists" %}</a></li>
|
||||||
|
<li class="is-active">
|
||||||
|
<a href="#" aria-current="page">
|
||||||
|
{{ list.name|truncatechars:30 }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% 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">
|
||||||
|
@ -56,37 +69,48 @@
|
||||||
<div>
|
<div>
|
||||||
{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}
|
{{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}
|
||||||
</div>
|
</div>
|
||||||
{% include 'snippets/shelve_button/shelve_button.html' %}
|
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<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">
|
||||||
<div>
|
<p>
|
||||||
<p>{% blocktrans with username=item.user.display_name user_path=item.user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p>
|
{% blocktrans trimmed with username=item.user.display_name user_path=item.user.local_path %}
|
||||||
</div>
|
Added by <a href="{{ user_path }}">{{ username }}</a>
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% if list.user == request.user or list.group|is_member:request.user %}
|
{% if list.user == request.user or list.group|is_member:request.user %}
|
||||||
<div class="card-footer-item">
|
<form
|
||||||
<form name="set-position" method="post" action="{% url 'list-set-book-position' item.id %}">
|
name="set-position-{{ item.id }}"
|
||||||
{% csrf_token %}
|
method="post"
|
||||||
<div class="field has-addons mb-0">
|
action="{% url 'list-set-book-position' item.id %}"
|
||||||
<div class="control">
|
class="card-footer-item"
|
||||||
<label for="input-list-position" class="button is-transparent is-small">{% trans "List position" %}</label>
|
>
|
||||||
</div>
|
{% csrf_token %}
|
||||||
<div class="control">
|
<div class="field has-addons mb-0">
|
||||||
<input id="input_list_position" class="input is-small" type="number" min="1" name="position" value="{{ item.order }}">
|
<div class="control">
|
||||||
</div>
|
<label for="input-list-position" class="button is-transparent is-small">{% trans "List position" %}</label>
|
||||||
<div class="control">
|
|
||||||
<button type="submit" class="button is-info is-small is-tablet">{% trans "Set" %}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<div class="control">
|
||||||
</div>
|
<input id="input_list_position_{{ item.id }}" class="input is-small" type="number" min="1" name="position" value="{{ item.order }}">
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<button type="submit" class="button is-info is-small is-tablet">{% trans "Set" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if list.user == request.user or list.curation == 'open' and item.user == request.user or list.group|is_member:request.user %}
|
{% if list.user == request.user or list.curation == 'open' and item.user == request.user or list.group|is_member:request.user %}
|
||||||
<form name="remove-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item">
|
<form
|
||||||
|
name="remove-book-{{ item.id }}"
|
||||||
|
method="post"
|
||||||
|
action="{% url 'list-remove-book' list.id %}"
|
||||||
|
class="card-footer-item"
|
||||||
|
>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="item" value="{{ item.id }}">
|
<input type="hidden" name="item" value="{{ item.id }}">
|
||||||
<button type="submit" class="button is-small is-danger">{% trans "Remove" %}</button>
|
<button type="submit" class="button is-small is-danger">{% trans "Remove" %}</button>
|
||||||
|
@ -172,14 +196,20 @@
|
||||||
|
|
||||||
<form
|
<form
|
||||||
class="mt-1"
|
class="mt-1"
|
||||||
name="add-book"
|
name="add-book-{{ book.id }}"
|
||||||
method="post"
|
method="post"
|
||||||
action="{% url 'list-add-book' %}{% if query %}?q={{ query }}{% endif %}"
|
action="{% url 'list-add-book' %}{% if query %}?q={{ query }}{% endif %}"
|
||||||
>
|
>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ book.id }}">
|
<input type="hidden" name="book" value="{{ book.id }}">
|
||||||
<input type="hidden" name="list" value="{{ list.id }}">
|
<input type="hidden" name="list" value="{{ list.id }}">
|
||||||
<button type="submit" class="button is-small 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="submit" class="button is-small 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>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -190,7 +220,18 @@
|
||||||
<h2 class="title is-5 mt-6" id="embed-label">
|
<h2 class="title is-5 mt-6" id="embed-label">
|
||||||
{% trans "Embed this list on a website" %}
|
{% trans "Embed this list on a website" %}
|
||||||
</h2>
|
</h2>
|
||||||
<textarea readonly class="textarea is-small" aria-labelledby="embed-label" data-copytext data-copytext-label="{% trans 'Copy embed code' %}" data-copytext-success="{% trans 'Copied!' %}"><iframe style="border-width:0;" id="bookwyrm_list_embed" width="400" height="600" title="{% blocktrans with list_name=list.name site_name=site.name owner=list.user.display_name %}{{ list_name }}, a list by {{owner}} on {{ site_name }}{% endblocktrans %}" src="{{ embed_url }}"></iframe></textarea>
|
<div class="vertical-copy">
|
||||||
|
<textarea
|
||||||
|
readonly
|
||||||
|
class="textarea is-small"
|
||||||
|
aria-labelledby="embed-label"
|
||||||
|
data-copytext
|
||||||
|
data-copytext-label="{% trans 'Copy embed code' %}"
|
||||||
|
data-copytext-success="{% trans 'Copied!' %}"
|
||||||
|
><iframe style="border-width:0;" id="bookwyrm_list_embed" width="400" height="600" title="{% blocktrans trimmed with list_name=list.name site_name=site.name owner=list.user.display_name %}
|
||||||
|
{{ list_name }}, a list by {{owner}} on {{ site_name }}
|
||||||
|
{% endblocktrans %}" src="{{ embed_url }}"></iframe></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
{% related_status notification as related_status %}
|
{% related_status notification as related_status %}
|
||||||
<div class="notification is-clickable {% if notification.id in unread %} is-primary{% endif %}" data-href="{% block primary_link %}{% endblock %}">
|
<div class="box is-shadowless has-background-white-ter {% if notification.id in unread %} is-primary{% endif %}">
|
||||||
<div class="columns is-mobile">
|
<div class="columns is-mobile">
|
||||||
<div class="column is-narrow is-size-3 {% if notification.id in unread%}has-text-white{% else %}has-text-grey{% endif %}">
|
<div class="column is-narrow is-size-3 {% if notification.id in unread%}has-text-white{% else %}has-text-grey{% endif %}">
|
||||||
{% block icon %}{% endblock %}
|
<a class="has-text-dark" href="{% block primary_link %}{% endblock %}">
|
||||||
|
{% block icon %}{% endblock %}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-clipped">
|
<div class="column is-clipped">
|
||||||
<div class="block">
|
<div class="block content">
|
||||||
<p>
|
<p>
|
||||||
{% if notification.related_user %}
|
{% if notification.related_user %}
|
||||||
<a href="{{ notification.related_user.local_path }}">{% include 'snippets/avatar.html' with user=notification.related_user %}
|
<a href="{{ notification.related_user.local_path }}">{% include 'snippets/avatar.html' with user=notification.related_user %}
|
||||||
|
@ -15,6 +18,7 @@
|
||||||
{% block description %}{% endblock %}
|
{% block description %}{% endblock %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if related_status %}
|
{% if related_status %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{% block preview %}{% endblock %}
|
{% block preview %}{% endblock %}
|
||||||
|
|
|
@ -46,7 +46,3 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script src="{% static "js/block_href.js" %}?v={{ js_cache }}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<ul class="block">
|
<ul class="block">
|
||||||
{% for result in local_results.results %}
|
{% for result in local_results.results %}
|
||||||
<li class="pd-4 mb-5">
|
<li class="pd-4 mb-5">
|
||||||
<div class="columns is-mobile is-gapless">
|
<div class="columns is-mobile is-gapless mb-0">
|
||||||
<div class="column is-cover">
|
<div class="column is-cover">
|
||||||
{% include 'snippets/book_cover.html' with book=result cover_class='is-w-xs is-h-xs' %}
|
{% include 'snippets/book_cover.html' with book=result cover_class='is-w-xs is-h-xs' %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,34 +34,28 @@
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{% for result_set in results|slice:"1:" %}
|
{% for result_set in results|slice:"1:" %}
|
||||||
{% if result_set.results %}
|
{% if result_set.results %}
|
||||||
<section class="box has-background-white-bis">
|
<section class="mb-5">
|
||||||
{% if not result_set.connector.local %}
|
{% if not result_set.connector.local %}
|
||||||
<header class="columns is-mobile">
|
<details class="details-panel box" {% if forloop.first %}open{% endif %}>
|
||||||
<div class="column">
|
{% endif %}
|
||||||
<h3 class="title is-5">
|
{% if not result_set.connector.local %}
|
||||||
|
<summary class="is-flex is-align-items-center is-flex-wrap-wrap is-gap-2">
|
||||||
|
<span class="mb-0 title is-5">
|
||||||
{% trans 'Results from' %}
|
{% trans 'Results from' %}
|
||||||
<a href="{{ result_set.connector.base_url }}" target="_blank">{{ result_set.connector.name|default:result_set.connector.identifier }}</a>
|
<a href="{{ result_set.connector.base_url }}" target="_blank">{{ result_set.connector.name|default:result_set.connector.identifier }}</a>
|
||||||
</h3>
|
</span>
|
||||||
</div>
|
|
||||||
<div class="column is-narrow">
|
|
||||||
{% trans "Open" as button_text %}
|
|
||||||
{% include 'snippets/toggle/open_button.html' with text=button_text controls_text="more_results_panel" controls_uid=result_set.connector.identifier class="is-small" icon_with_text="arrow-down" pressed=forloop.first %}
|
|
||||||
{% trans "Close" as button_text %}
|
|
||||||
{% include 'snippets/toggle/close_button.html' with text=button_text controls_text="more_results_panel" controls_uid=result_set.connector.identifier class="is-small" icon_with_text="arrow-up" pressed=forloop.first %}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="box has-background-white is-shadowless{% if not forloop.first %} is-hidden{% endif %}" id="more_results_panel_{{ result_set.connector.identifier }}">
|
<span class="details-close icon icon-x" aria-hidden></span>
|
||||||
|
</summary>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mt-5">
|
||||||
<div class="is-flex is-flex-direction-row-reverse">
|
<div class="is-flex is-flex-direction-row-reverse">
|
||||||
<div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="is-flex-grow-1">
|
<ul class="is-flex-grow-1">
|
||||||
{% for result in result_set.results %}
|
{% for result in result_set.results %}
|
||||||
<li class="mb-5">
|
<li class="{% if not forloop.last %}mb-5{% endif %}">
|
||||||
<div class="columns is-mobile is-gapless">
|
<div class="columns is-mobile is-gapless">
|
||||||
<div class="columns is-mobile is-gapless">
|
<div class="column is-1 is-cover">
|
||||||
{% include 'snippets/book_cover.html' with book=result cover_class='is-w-xs is-h-xs' external_path=True %}
|
{% include 'snippets/book_cover.html' with book=result cover_class='is-w-xs is-h-xs' external_path=True %}
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-10 ml-3">
|
<div class="column is-10 ml-3">
|
||||||
|
@ -92,6 +86,9 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if not result_set.connector.local %}
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<form class="block" action="{% url 'search' %}" method="GET">
|
<form class="block" action="{% url 'search' %}" method="GET">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input type="input" class="input" name="q" value="{{ query }}" aria-label="{% trans 'Search query' %}">
|
<input type="text" class="input" name="q" value="{{ query }}" aria-label="{% trans 'Search query' %}">
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<div class="select" aria-label="{% trans 'Search type' %}">
|
<div class="select" aria-label="{% trans 'Search type' %}">
|
||||||
|
|
|
@ -31,35 +31,29 @@
|
||||||
|
|
||||||
<div class="block content">
|
<div class="block content">
|
||||||
<dl>
|
<dl>
|
||||||
<div class="is-flex notification pt-1 pb-1 mb-0 {% if announcement in active_announcements %}is-success{% else %}is-danger{% endif %}">
|
<dt class="is-pulled-left mr-5 has-text-weight-bold">{% trans "Visible:" %}</dt>
|
||||||
<dt class="mr-1 has-text-weight-bold">{% trans "Visible:" %}</dt>
|
<dd>
|
||||||
<dd>
|
<span class="tag {% if announcement in active_announcements %}is-success{% else %}is-danger{% endif %}">
|
||||||
{% if announcement in active_announcements %}
|
{% if announcement in active_announcements %}
|
||||||
{% trans "True" %}
|
{% trans "True" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans "False" %}
|
{% trans "False" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</span>
|
||||||
</div>
|
</dd>
|
||||||
|
|
||||||
{% if announcement.start_date %}
|
{% if announcement.start_date %}
|
||||||
<div class="is-flex notificationi pt-1 pb-1 mb-0 has-background-white">
|
<dt class="is-pulled-left mr-5 has-text-weight-bold">{% trans "Start date:" %}</dt>
|
||||||
<dt class="mr-1 has-text-weight-bold">{% trans "Start date:" %}</dt>
|
<dd>{{ announcement.start_date|naturalday }}</dd>
|
||||||
<dd>{{ announcement.start_date|naturalday }}</dd>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if announcement.end_date %}
|
{% if announcement.end_date %}
|
||||||
<div class="is-flex notification pt-1 pb-1 mb-0 has-background-white">
|
<dt class="is-pulled-left mr-5 has-text-weight-bold">{% trans "End date:" %}</dt>
|
||||||
<dt class="mr-1 has-text-weight-bold">{% trans "End date:" %}</dt>
|
<dd>{{ announcement.end_date|naturalday }}</dd>
|
||||||
<dd>{{ announcement.end_date|naturalday }}</dd>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="is-flex notification pt-1 pb-1 has-background-white">
|
<dt class="is-pulled-left mr-5 has-text-weight-bold">{% trans "Active:" %}</dt>
|
||||||
<dt class="mr-1 has-text-weight-bold">{% trans "Active:" %}</dt>
|
<dd>{{ announcement.active }}</dd>
|
||||||
<dd>{{ announcement.active }}</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<hr aria-hidden="true">
|
<hr aria-hidden="true">
|
||||||
|
|
|
@ -3,5 +3,7 @@
|
||||||
|
|
||||||
{% block filter %}
|
{% block filter %}
|
||||||
<label class="label" for="id_server">{% trans "Instance name" %}</label>
|
<label class="label" for="id_server">{% trans "Instance name" %}</label>
|
||||||
<input type="text" class="input" name="server" value="{{ request.GET.server|default:'' }}" id="id_server" placeholder="example.server.com">
|
<div class="control">
|
||||||
|
<input type="text" class="input" name="server" value="{{ request.GET.server|default:'' }}" id="id_server" placeholder="example.server.com">
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
{% block filter %}
|
{% block filter %}
|
||||||
<label class="label" for="id_username">{% trans "Username" %}</label>
|
<label class="label" for="id_username">{% trans "Username" %}</label>
|
||||||
<input type="text" class="input" name="username" value="{{ request.GET.username|default:'' }}" id="id_username" placeholder="user@domain.com">
|
<div class="control">
|
||||||
|
<input type="text" class="input" name="username" value="{{ request.GET.username|default:'' }}" id="id_username" placeholder="user@domain.com">
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-narrow is-hidden-mobile">
|
<div class="column is-narrow is-hidden-mobile">
|
||||||
<figure class="block is-w-xl">
|
<figure class="block is-w-{% if size %}{{ size }}{% else %}xl{% endif %}">
|
||||||
<img src="{% if site.logo %}{% get_media_prefix %}{{ site.logo }}{% else %}{% static "images/logo.png" %}{% endif %}" alt="BookWyrm logo">
|
<img src="{% if site.logo %}{% get_media_prefix %}{{ site.logo }}{% else %}{% static "images/logo.png" %}{% endif %}" alt="BookWyrm logo">
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
{% for author in book.authors.all|slice:limit %}
|
{% for author in book.authors.all|slice:limit %}
|
||||||
<a
|
<a
|
||||||
href="{{ author.local_path }}"
|
href="{{ author.local_path }}"
|
||||||
class="author"
|
class="author {{ link_class }}"
|
||||||
itemprop="author"
|
itemprop="author"
|
||||||
itemscope
|
itemscope
|
||||||
itemtype="https://schema.org/Thing"
|
itemtype="https://schema.org/Thing"
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load utilities %}
|
{% load utilities %}
|
||||||
|
{% load cache %}
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
|
|
||||||
|
{# 6 month cache #}
|
||||||
|
{% cache 15552000 titleby book.id %}
|
||||||
|
|
||||||
{% if book.authors.exists %}
|
{% if book.authors.exists %}
|
||||||
{% blocktrans trimmed with path=book.local_path title=book|book_title %}
|
{% blocktrans trimmed with path=book.local_path title=book|book_title %}
|
||||||
<a href="{{ path }}">{{ title }}</a> by
|
<a href="{{ path }}">{{ title }}</a> by
|
||||||
|
@ -10,4 +14,6 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ book.local_path }}">{{ book|book_title }}</a>
|
<a href="{{ book.local_path }}">{{ book|book_title }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% endcache %}
|
||||||
{% endspaceless %}
|
{% endspaceless %}
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
<div class="column is-flex">
|
<div class="filters-field column">
|
||||||
<div class="box is-flex-grow-1">
|
{% block filter %}
|
||||||
{% block filter %}
|
{% endblock %}
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,28 +1,47 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="notification content">
|
<details class="details-panel box is-size-{{ size|default:'normal' }}" {% if filters_applied %}open{% endif %}>
|
||||||
<h2 class="columns is-mobile mb-0">
|
<summary class="is-flex is-align-items-center is-flex-wrap-wrap is-gap-2">
|
||||||
<span class="column pb-0">Filters</span>
|
<span class="mb-0 title {% if size == 'small' %}is-6{% else %}is-5{% endif %} is-flex-shrink-0">
|
||||||
|
{% trans "Filters" %}
|
||||||
|
|
||||||
<span class="column is-narrow pb-0">
|
|
||||||
{% trans "Show filters" as text %}
|
|
||||||
{% include 'snippets/toggle/open_button.html' with text=text controls_text="filters" icon_with_text="arrow-down" class="is-small" focus="filters" %}
|
|
||||||
{% trans "Hide filters" as text %}
|
|
||||||
{% include 'snippets/toggle/close_button.html' with text=text controls_text="filters" icon_with_text="arrow-up" class="is-small" %}
|
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
|
||||||
|
|
||||||
<form class="is-hidden mt-3" id="filters" method="get" action="{{ request.path }}" tabindex="0">
|
{% if filters_applied %}
|
||||||
{% if sort %}
|
<span class="tag is-success is-light ml-2 mb-0 is-{{ size|default:'normal' }}">
|
||||||
<input type="hidden" name="sort" value="{{ sort }}">
|
{{ _("Filters are applied") }}
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="columns">
|
|
||||||
{% block filter_fields %}
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
<button class="button is-primary">{% trans "Apply filters" %}</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% if request.GET %}
|
{% if request.GET %}
|
||||||
<a class="help" href="{{ request.path }}">{% trans "Clear filters" %}</a>
|
<span class="mb-0 tags has-addons">
|
||||||
{% endif %}
|
<span class="mb-0 tag is-success is-light is-{{ size|default:'normal' }}">
|
||||||
</div>
|
{% trans "Filters are applied" %}
|
||||||
|
</span>
|
||||||
|
<a class="mb-0 tag is-success is-{{ size|default:'normal' }}" href="{{ request.path }}">
|
||||||
|
{% trans "Clear filters" %}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<span class="details-close icon icon-x is-{{ size|default:'normal' }}" aria-hidden></span>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<form id="filters" method="{{ method|default:'get' }}" action="{{ action|default:request.path }}">
|
||||||
|
{% if method == 'post' %}
|
||||||
|
{% csrf_token %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if sort %}
|
||||||
|
<input type="hidden" name="sort" value="{{ sort }}">
|
||||||
|
{% endif %}
|
||||||
|
<div class="mt-3 columns filters-fields is-align-items-stretch">
|
||||||
|
{% block filter_fields %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="button is-primary is-small">
|
||||||
|
{% trans "Apply filters" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% if request.user == user or not request.user.is_authenticated %}
|
{% if request.user == user or not request.user.is_authenticated %}
|
||||||
{% elif user in request.user.blocks.all %}
|
{% elif user in request.user.blocks.all %}
|
||||||
{% include 'snippets/block_button.html' with blocks=True %}
|
{% include 'snippets/block_button.html' with blocks=True %}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
type="number"
|
type="number"
|
||||||
name="progress"
|
name="progress"
|
||||||
class="input"
|
class="input"
|
||||||
id="id_progress_{{ readthrough.id }}{{ controls_uid }}"
|
id="{{ field_id }}"
|
||||||
value="{{ readthrough.progress }}"
|
value="{{ readthrough.progress }}"
|
||||||
{% if progress_required %}required{% endif %}
|
{% if progress_required %}required{% endif %}
|
||||||
>
|
>
|
||||||
|
|
|
@ -9,7 +9,7 @@ Finish "<em>{{ book_title }}</em>"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal-form-open %}
|
{% block modal-form-open %}
|
||||||
<form name="finish-reading" action="{% url 'reading-status' 'finish' book.id %}" method="post" {% if not refresh %}class="submit-status"{% endif %}>
|
<form name="finish-reading-{{ uuid }}" action="{% url 'reading-status' 'finish' book.id %}" method="post" {% if not refresh %}class="submit-status"{% endif %}>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
||||||
<input type="hidden" name="reading_status" value="read">
|
<input type="hidden" name="reading_status" value="read">
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue