forked from mirrors/bookwyrm
Merge branch 'main' into add-edition
This commit is contained in:
commit
68dc5962ee
153 changed files with 10991 additions and 6067 deletions
|
@ -1,3 +0,0 @@
|
|||
@charset "utf-8";
|
||||
|
||||
// Copy this file to bookwyrm/static/css/ and set your instance custom styles.
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -17,7 +17,8 @@
|
|||
.env
|
||||
/images/
|
||||
bookwyrm/static/css/bookwyrm.css
|
||||
bookwyrm/static/css/_instance-settings.scss
|
||||
bookwyrm/static/css/themes/
|
||||
!bookwyrm/static/css/themes/bookwyrm-*.scss
|
||||
|
||||
# Testing
|
||||
.coverage
|
||||
|
|
1
.prettierignore
Normal file
1
.prettierignore
Normal file
|
@ -0,0 +1 @@
|
|||
**/vendor/*
|
|
@ -13,7 +13,7 @@ Social reading and reviewing, decentralized with ActivityPub
|
|||
- [Set up Bookwyrm](#set-up-bookwyrm)
|
||||
|
||||
## Joining BookWyrm
|
||||
BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://docs.joinbookwyrm.com/instances.html) list.
|
||||
BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://joinbookwyrm.com/instances/) list.
|
||||
|
||||
You can request an invite by entering your email address at https://bookwyrm.social.
|
||||
|
||||
|
|
|
@ -39,4 +39,5 @@ class Person(ActivityObject):
|
|||
bookwyrmUser: bool = False
|
||||
manuallyApprovesFollowers: str = False
|
||||
discoverable: str = False
|
||||
hideFollows: str = False
|
||||
type: str = "Person"
|
||||
|
|
|
@ -19,11 +19,11 @@ def download_file(url, destination):
|
|||
with open(destination, "b+w") as outfile:
|
||||
outfile.write(stream.read())
|
||||
except (urllib.error.HTTPError, urllib.error.URLError):
|
||||
logger.error("Failed to download file %s", url)
|
||||
logger.info("Failed to download file %s", url)
|
||||
except OSError:
|
||||
logger.error("Couldn't open font file %s for writing", destination)
|
||||
logger.info("Couldn't open font file %s for writing", destination)
|
||||
except: # pylint: disable=bare-except
|
||||
logger.exception("Unknown error in file download")
|
||||
logger.info("Unknown error in file download")
|
||||
|
||||
|
||||
class BookwyrmConfig(AppConfig):
|
||||
|
|
|
@ -131,7 +131,7 @@ class AbstractConnector(AbstractMinimalConnector):
|
|||
try:
|
||||
work_data = self.get_work_from_edition_data(data)
|
||||
except (KeyError, ConnectorException) as err:
|
||||
logger.exception(err)
|
||||
logger.info(err)
|
||||
work_data = data
|
||||
|
||||
if not work_data or not edition_data:
|
||||
|
@ -270,7 +270,7 @@ def get_data(url, params=None, timeout=10):
|
|||
timeout=timeout,
|
||||
)
|
||||
except RequestException as err:
|
||||
logger.exception(err)
|
||||
logger.info(err)
|
||||
raise ConnectorException(err)
|
||||
|
||||
if not resp.ok:
|
||||
|
@ -278,7 +278,7 @@ def get_data(url, params=None, timeout=10):
|
|||
try:
|
||||
data = resp.json()
|
||||
except ValueError as err:
|
||||
logger.exception(err)
|
||||
logger.info(err)
|
||||
raise ConnectorException(err)
|
||||
|
||||
return data
|
||||
|
@ -296,7 +296,7 @@ def get_image(url, timeout=10):
|
|||
timeout=timeout,
|
||||
)
|
||||
except RequestException as err:
|
||||
logger.exception(err)
|
||||
logger.info(err)
|
||||
return None, None
|
||||
|
||||
if not resp.ok:
|
||||
|
@ -305,7 +305,7 @@ def get_image(url, timeout=10):
|
|||
image_content = ContentFile(resp.content)
|
||||
extension = imghdr.what(None, image_content.read())
|
||||
if not extension:
|
||||
logger.exception("File requested was not an image: %s", url)
|
||||
logger.info("File requested was not an image: %s", url)
|
||||
return None, None
|
||||
|
||||
return image_content, extension
|
||||
|
|
|
@ -39,7 +39,7 @@ def search(query, min_confidence=0.1, return_first=False):
|
|||
try:
|
||||
result_set = connector.isbn_search(isbn)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
logger.exception(err)
|
||||
logger.info(err)
|
||||
# if this fails, we can still try regular search
|
||||
|
||||
# if no isbn search results, we fallback to generic search
|
||||
|
@ -48,7 +48,7 @@ def search(query, min_confidence=0.1, return_first=False):
|
|||
result_set = connector.search(query, min_confidence=min_confidence)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
# we don't want *any* error to crash the whole search page
|
||||
logger.exception(err)
|
||||
logger.info(err)
|
||||
continue
|
||||
|
||||
if return_first and result_set:
|
||||
|
|
|
@ -8,8 +8,20 @@ def site_settings(request): # pylint: disable=unused-argument
|
|||
if not request.is_secure():
|
||||
request_protocol = "http://"
|
||||
|
||||
site = models.SiteSettings.objects.get()
|
||||
theme = "css/themes/bookwyrm-light.scss"
|
||||
if (
|
||||
hasattr(request, "user")
|
||||
and request.user.is_authenticated
|
||||
and request.user.theme
|
||||
):
|
||||
theme = request.user.theme.path
|
||||
elif site.default_theme:
|
||||
theme = site.default_theme.path
|
||||
|
||||
return {
|
||||
"site": models.SiteSettings.objects.get(),
|
||||
"site": site,
|
||||
"site_theme": theme,
|
||||
"active_announcements": models.Announcement.active_announcements(),
|
||||
"thumbnail_generation_enabled": settings.ENABLE_THUMBNAIL_GENERATION,
|
||||
"media_full_url": settings.MEDIA_FULL_URL,
|
||||
|
|
|
@ -1,582 +0,0 @@
|
|||
""" using django model forms """
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django import forms
|
||||
from django.forms import ModelForm, PasswordInput, widgets, ChoiceField
|
||||
from django.forms.widgets import Textarea
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models.fields import ClearableFileInputWithWarning
|
||||
from bookwyrm.models.user import FeedFilterChoices
|
||||
|
||||
|
||||
class CustomForm(ModelForm):
|
||||
"""add css classes to the forms"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
css_classes = defaultdict(lambda: "")
|
||||
css_classes["text"] = "input"
|
||||
css_classes["password"] = "input"
|
||||
css_classes["email"] = "input"
|
||||
css_classes["number"] = "input"
|
||||
css_classes["checkbox"] = "checkbox"
|
||||
css_classes["textarea"] = "textarea"
|
||||
# pylint: disable=super-with-arguments
|
||||
super(CustomForm, self).__init__(*args, **kwargs)
|
||||
for visible in self.visible_fields():
|
||||
if hasattr(visible.field.widget, "input_type"):
|
||||
input_type = visible.field.widget.input_type
|
||||
if isinstance(visible.field.widget, Textarea):
|
||||
input_type = "textarea"
|
||||
visible.field.widget.attrs["rows"] = 5
|
||||
visible.field.widget.attrs["class"] = css_classes[input_type]
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
class LoginForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["localname", "password"]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
"password": PasswordInput(),
|
||||
}
|
||||
|
||||
|
||||
class RegisterForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["localname", "email", "password"]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {"password": PasswordInput()}
|
||||
|
||||
def clean(self):
|
||||
"""Check if the username is taken"""
|
||||
cleaned_data = super().clean()
|
||||
localname = cleaned_data.get("localname").strip()
|
||||
if models.User.objects.filter(localname=localname).first():
|
||||
self.add_error("localname", _("User with this username already exists"))
|
||||
|
||||
|
||||
class RatingForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.ReviewRating
|
||||
fields = ["user", "book", "rating", "privacy"]
|
||||
|
||||
|
||||
class ReviewForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Review
|
||||
fields = [
|
||||
"user",
|
||||
"book",
|
||||
"name",
|
||||
"content",
|
||||
"rating",
|
||||
"content_warning",
|
||||
"sensitive",
|
||||
"privacy",
|
||||
]
|
||||
|
||||
|
||||
class CommentForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Comment
|
||||
fields = [
|
||||
"user",
|
||||
"book",
|
||||
"content",
|
||||
"content_warning",
|
||||
"sensitive",
|
||||
"privacy",
|
||||
"progress",
|
||||
"progress_mode",
|
||||
"reading_status",
|
||||
]
|
||||
|
||||
|
||||
class QuotationForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Quotation
|
||||
fields = [
|
||||
"user",
|
||||
"book",
|
||||
"quote",
|
||||
"content",
|
||||
"content_warning",
|
||||
"sensitive",
|
||||
"privacy",
|
||||
"position",
|
||||
"position_mode",
|
||||
]
|
||||
|
||||
|
||||
class ReplyForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Status
|
||||
fields = [
|
||||
"user",
|
||||
"content",
|
||||
"content_warning",
|
||||
"sensitive",
|
||||
"reply_parent",
|
||||
"privacy",
|
||||
]
|
||||
|
||||
|
||||
class StatusForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Status
|
||||
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
|
||||
|
||||
|
||||
class DirectForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Status
|
||||
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
|
||||
|
||||
|
||||
class EditUserForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = [
|
||||
"avatar",
|
||||
"name",
|
||||
"email",
|
||||
"summary",
|
||||
"show_goal",
|
||||
"show_suggested_users",
|
||||
"manually_approves_followers",
|
||||
"default_post_privacy",
|
||||
"discoverable",
|
||||
"preferred_timezone",
|
||||
"preferred_language",
|
||||
]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
"avatar": ClearableFileInputWithWarning(
|
||||
attrs={"aria-describedby": "desc_avatar"}
|
||||
),
|
||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
||||
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
|
||||
"email": forms.EmailInput(attrs={"aria-describedby": "desc_email"}),
|
||||
"discoverable": forms.CheckboxInput(
|
||||
attrs={"aria-describedby": "desc_discoverable"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class LimitedEditUserForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = [
|
||||
"avatar",
|
||||
"name",
|
||||
"summary",
|
||||
"manually_approves_followers",
|
||||
"discoverable",
|
||||
]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
"avatar": ClearableFileInputWithWarning(
|
||||
attrs={"aria-describedby": "desc_avatar"}
|
||||
),
|
||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
||||
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
|
||||
"discoverable": forms.CheckboxInput(
|
||||
attrs={"aria-describedby": "desc_discoverable"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class DeleteUserForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["password"]
|
||||
|
||||
|
||||
class UserGroupForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["groups"]
|
||||
|
||||
|
||||
class FeedStatusTypesForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["feed_status_types"]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
"feed_status_types": widgets.CheckboxSelectMultiple(
|
||||
choices=FeedFilterChoices,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class CoverForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Book
|
||||
fields = ["cover"]
|
||||
help_texts = {f: None for f in fields}
|
||||
|
||||
|
||||
class LinkDomainForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.LinkDomain
|
||||
fields = ["name"]
|
||||
|
||||
|
||||
class FileLinkForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.FileLink
|
||||
fields = ["url", "filetype", "availability", "book", "added_by"]
|
||||
|
||||
def clean(self):
|
||||
"""make sure the domain isn't blocked or pending"""
|
||||
cleaned_data = super().clean()
|
||||
url = cleaned_data.get("url")
|
||||
filetype = cleaned_data.get("filetype")
|
||||
book = cleaned_data.get("book")
|
||||
domain = urlparse(url).netloc
|
||||
if models.LinkDomain.objects.filter(domain=domain).exists():
|
||||
status = models.LinkDomain.objects.get(domain=domain).status
|
||||
if status == "blocked":
|
||||
# pylint: disable=line-too-long
|
||||
self.add_error(
|
||||
"url",
|
||||
_(
|
||||
"This domain is blocked. Please contact your administrator if you think this is an error."
|
||||
),
|
||||
)
|
||||
elif models.FileLink.objects.filter(
|
||||
url=url, book=book, filetype=filetype
|
||||
).exists():
|
||||
# pylint: disable=line-too-long
|
||||
self.add_error(
|
||||
"url",
|
||||
_(
|
||||
"This link with file type has already been added for this book. If it is not visible, the domain is still pending."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class EditionFromWorkForm(CustomForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# make all fields hidden
|
||||
for visible in self.visible_fields():
|
||||
visible.field.widget = forms.HiddenInput()
|
||||
|
||||
class Meta:
|
||||
model = models.Work
|
||||
fields = [
|
||||
"title",
|
||||
"subtitle",
|
||||
"authors",
|
||||
"description",
|
||||
"languages",
|
||||
"series",
|
||||
"series_number",
|
||||
"subjects",
|
||||
"subject_places",
|
||||
"cover",
|
||||
"first_published_date",
|
||||
]
|
||||
|
||||
|
||||
class EditionForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Edition
|
||||
exclude = [
|
||||
"authors",
|
||||
"parent_work",
|
||||
"remote_id",
|
||||
"origin_id",
|
||||
"created_date",
|
||||
"updated_date",
|
||||
"edition_rank",
|
||||
"shelves",
|
||||
"connector",
|
||||
"search_vector",
|
||||
"links",
|
||||
"file_links",
|
||||
]
|
||||
widgets = {
|
||||
"title": forms.TextInput(attrs={"aria-describedby": "desc_title"}),
|
||||
"subtitle": forms.TextInput(attrs={"aria-describedby": "desc_subtitle"}),
|
||||
"description": forms.Textarea(
|
||||
attrs={"aria-describedby": "desc_description"}
|
||||
),
|
||||
"series": forms.TextInput(attrs={"aria-describedby": "desc_series"}),
|
||||
"series_number": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_series_number"}
|
||||
),
|
||||
"languages": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_languages_help desc_languages"}
|
||||
),
|
||||
"publishers": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
|
||||
),
|
||||
"first_published_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_first_published_date"}
|
||||
),
|
||||
"published_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_published_date"}
|
||||
),
|
||||
"cover": ClearableFileInputWithWarning(
|
||||
attrs={"aria-describedby": "desc_cover"}
|
||||
),
|
||||
"physical_format": forms.Select(
|
||||
attrs={"aria-describedby": "desc_physical_format"}
|
||||
),
|
||||
"physical_format_detail": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_physical_format_detail"}
|
||||
),
|
||||
"pages": forms.NumberInput(attrs={"aria-describedby": "desc_pages"}),
|
||||
"isbn_13": forms.TextInput(attrs={"aria-describedby": "desc_isbn_13"}),
|
||||
"isbn_10": forms.TextInput(attrs={"aria-describedby": "desc_isbn_10"}),
|
||||
"openlibrary_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_openlibrary_key"}
|
||||
),
|
||||
"inventaire_id": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_inventaire_id"}
|
||||
),
|
||||
"oclc_number": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_oclc_number"}
|
||||
),
|
||||
"ASIN": forms.TextInput(attrs={"aria-describedby": "desc_ASIN"}),
|
||||
}
|
||||
|
||||
|
||||
class AuthorForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Author
|
||||
fields = [
|
||||
"last_edited_by",
|
||||
"name",
|
||||
"aliases",
|
||||
"bio",
|
||||
"wikipedia_link",
|
||||
"born",
|
||||
"died",
|
||||
"openlibrary_key",
|
||||
"inventaire_id",
|
||||
"librarything_key",
|
||||
"goodreads_key",
|
||||
"isni",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
||||
"aliases": forms.TextInput(attrs={"aria-describedby": "desc_aliases"}),
|
||||
"bio": forms.Textarea(attrs={"aria-describedby": "desc_bio"}),
|
||||
"wikipedia_link": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_wikipedia_link"}
|
||||
),
|
||||
"born": forms.SelectDateWidget(attrs={"aria-describedby": "desc_born"}),
|
||||
"died": forms.SelectDateWidget(attrs={"aria-describedby": "desc_died"}),
|
||||
"oepnlibrary_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_oepnlibrary_key"}
|
||||
),
|
||||
"inventaire_id": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_inventaire_id"}
|
||||
),
|
||||
"librarything_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_librarything_key"}
|
||||
),
|
||||
"goodreads_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_goodreads_key"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class ImportForm(forms.Form):
|
||||
csv_file = forms.FileField()
|
||||
|
||||
|
||||
class ExpiryWidget(widgets.Select):
|
||||
def value_from_datadict(self, data, files, name):
|
||||
"""human-readable exiration time buckets"""
|
||||
selected_string = super().value_from_datadict(data, files, name)
|
||||
|
||||
if selected_string == "day":
|
||||
interval = datetime.timedelta(days=1)
|
||||
elif selected_string == "week":
|
||||
interval = datetime.timedelta(days=7)
|
||||
elif selected_string == "month":
|
||||
interval = datetime.timedelta(days=31) # Close enough?
|
||||
elif selected_string == "forever":
|
||||
return None
|
||||
else:
|
||||
return selected_string # This will raise
|
||||
|
||||
return timezone.now() + interval
|
||||
|
||||
|
||||
class InviteRequestForm(CustomForm):
|
||||
def clean(self):
|
||||
"""make sure the email isn't in use by a registered user"""
|
||||
cleaned_data = super().clean()
|
||||
email = cleaned_data.get("email")
|
||||
if email and models.User.objects.filter(email=email).exists():
|
||||
self.add_error("email", _("A user with this email already exists."))
|
||||
|
||||
class Meta:
|
||||
model = models.InviteRequest
|
||||
fields = ["email"]
|
||||
|
||||
|
||||
class CreateInviteForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.SiteInvite
|
||||
exclude = ["code", "user", "times_used", "invitees"]
|
||||
widgets = {
|
||||
"expiry": ExpiryWidget(
|
||||
choices=[
|
||||
("day", _("One Day")),
|
||||
("week", _("One Week")),
|
||||
("month", _("One Month")),
|
||||
("forever", _("Does Not Expire")),
|
||||
]
|
||||
),
|
||||
"use_limit": widgets.Select(
|
||||
choices=[(i, _(f"{i} uses")) for i in [1, 5, 10, 25, 50, 100]]
|
||||
+ [(None, _("Unlimited"))]
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class ShelfForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Shelf
|
||||
fields = ["user", "name", "privacy", "description"]
|
||||
|
||||
|
||||
class GoalForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.AnnualGoal
|
||||
fields = ["user", "year", "goal", "privacy"]
|
||||
|
||||
|
||||
class SiteForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.SiteSettings
|
||||
exclude = ["admin_code", "install_mode"]
|
||||
widgets = {
|
||||
"instance_short_description": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_instance_short_description"}
|
||||
),
|
||||
"require_confirm_email": forms.CheckboxInput(
|
||||
attrs={"aria-describedby": "desc_require_confirm_email"}
|
||||
),
|
||||
"invite_request_text": forms.Textarea(
|
||||
attrs={"aria-describedby": "desc_invite_request_text"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class AnnouncementForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Announcement
|
||||
exclude = ["remote_id"]
|
||||
widgets = {
|
||||
"preview": forms.TextInput(attrs={"aria-describedby": "desc_preview"}),
|
||||
"content": forms.Textarea(attrs={"aria-describedby": "desc_content"}),
|
||||
"event_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_event_date"}
|
||||
),
|
||||
"start_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_start_date"}
|
||||
),
|
||||
"end_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_end_date"}
|
||||
),
|
||||
"active": forms.CheckboxInput(attrs={"aria-describedby": "desc_active"}),
|
||||
}
|
||||
|
||||
|
||||
class ListForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.List
|
||||
fields = ["user", "name", "description", "curation", "privacy", "group"]
|
||||
|
||||
|
||||
class ListItemForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.ListItem
|
||||
fields = ["user", "book", "book_list", "notes"]
|
||||
|
||||
|
||||
class GroupForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Group
|
||||
fields = ["user", "privacy", "name", "description"]
|
||||
|
||||
|
||||
class ReportForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Report
|
||||
fields = ["user", "reporter", "status", "links", "note"]
|
||||
|
||||
|
||||
class EmailBlocklistForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.EmailBlocklist
|
||||
fields = ["domain"]
|
||||
widgets = {
|
||||
"avatar": forms.TextInput(attrs={"aria-describedby": "desc_domain"}),
|
||||
}
|
||||
|
||||
|
||||
class IPBlocklistForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.IPBlocklist
|
||||
fields = ["address"]
|
||||
|
||||
|
||||
class ServerForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.FederatedServer
|
||||
exclude = ["remote_id"]
|
||||
|
||||
|
||||
class SortListForm(forms.Form):
|
||||
sort_by = ChoiceField(
|
||||
choices=(
|
||||
("order", _("List Order")),
|
||||
("title", _("Book Title")),
|
||||
("rating", _("Rating")),
|
||||
),
|
||||
label=_("Sort By"),
|
||||
)
|
||||
direction = ChoiceField(
|
||||
choices=(
|
||||
("ascending", _("Ascending")),
|
||||
("descending", _("Descending")),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ReadThroughForm(CustomForm):
|
||||
def clean(self):
|
||||
"""make sure the email isn't in use by a registered user"""
|
||||
cleaned_data = super().clean()
|
||||
start_date = cleaned_data.get("start_date")
|
||||
finish_date = cleaned_data.get("finish_date")
|
||||
if start_date and finish_date and start_date > finish_date:
|
||||
self.add_error(
|
||||
"finish_date", _("Reading finish date cannot be before start date.")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.ReadThrough
|
||||
fields = ["user", "book", "start_date", "finish_date"]
|
||||
|
||||
|
||||
class AutoModRuleForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.AutoMod
|
||||
fields = ["string_match", "flag_users", "flag_statuses", "created_by"]
|
12
bookwyrm/forms/__init__.py
Normal file
12
bookwyrm/forms/__init__.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
""" make forms available to the app """
|
||||
# site admin
|
||||
from .admin import *
|
||||
from .author import *
|
||||
from .books import *
|
||||
from .edit_user import *
|
||||
from .forms import *
|
||||
from .groups import *
|
||||
from .landing import *
|
||||
from .links import *
|
||||
from .lists import *
|
||||
from .status import *
|
141
bookwyrm/forms/admin.py
Normal file
141
bookwyrm/forms/admin.py
Normal file
|
@ -0,0 +1,141 @@
|
|||
""" using django model forms """
|
||||
import datetime
|
||||
|
||||
from django import forms
|
||||
from django.forms import widgets
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_celery_beat.models import IntervalSchedule
|
||||
|
||||
from bookwyrm import models
|
||||
from .custom_form import CustomForm
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
class ExpiryWidget(widgets.Select):
|
||||
def value_from_datadict(self, data, files, name):
|
||||
"""human-readable exiration time buckets"""
|
||||
selected_string = super().value_from_datadict(data, files, name)
|
||||
|
||||
if selected_string == "day":
|
||||
interval = datetime.timedelta(days=1)
|
||||
elif selected_string == "week":
|
||||
interval = datetime.timedelta(days=7)
|
||||
elif selected_string == "month":
|
||||
interval = datetime.timedelta(days=31) # Close enough?
|
||||
elif selected_string == "forever":
|
||||
return None
|
||||
else:
|
||||
return selected_string # This will raise
|
||||
|
||||
return timezone.now() + interval
|
||||
|
||||
|
||||
class CreateInviteForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.SiteInvite
|
||||
exclude = ["code", "user", "times_used", "invitees"]
|
||||
widgets = {
|
||||
"expiry": ExpiryWidget(
|
||||
choices=[
|
||||
("day", _("One Day")),
|
||||
("week", _("One Week")),
|
||||
("month", _("One Month")),
|
||||
("forever", _("Does Not Expire")),
|
||||
]
|
||||
),
|
||||
"use_limit": widgets.Select(
|
||||
choices=[(i, _(f"{i} uses")) for i in [1, 5, 10, 25, 50, 100]]
|
||||
+ [(None, _("Unlimited"))]
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class SiteForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.SiteSettings
|
||||
exclude = ["admin_code", "install_mode"]
|
||||
widgets = {
|
||||
"instance_short_description": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_instance_short_description"}
|
||||
),
|
||||
"require_confirm_email": forms.CheckboxInput(
|
||||
attrs={"aria-describedby": "desc_require_confirm_email"}
|
||||
),
|
||||
"invite_request_text": forms.Textarea(
|
||||
attrs={"aria-describedby": "desc_invite_request_text"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class ThemeForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Theme
|
||||
fields = ["name", "path"]
|
||||
widgets = {
|
||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
||||
"path": forms.TextInput(
|
||||
attrs={
|
||||
"aria-describedby": "desc_path",
|
||||
"placeholder": "css/themes/theme-name.scss",
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class AnnouncementForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Announcement
|
||||
exclude = ["remote_id"]
|
||||
widgets = {
|
||||
"preview": forms.TextInput(attrs={"aria-describedby": "desc_preview"}),
|
||||
"content": forms.Textarea(attrs={"aria-describedby": "desc_content"}),
|
||||
"event_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_event_date"}
|
||||
),
|
||||
"start_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_start_date"}
|
||||
),
|
||||
"end_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_end_date"}
|
||||
),
|
||||
"active": forms.CheckboxInput(attrs={"aria-describedby": "desc_active"}),
|
||||
}
|
||||
|
||||
|
||||
class EmailBlocklistForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.EmailBlocklist
|
||||
fields = ["domain"]
|
||||
widgets = {
|
||||
"avatar": forms.TextInput(attrs={"aria-describedby": "desc_domain"}),
|
||||
}
|
||||
|
||||
|
||||
class IPBlocklistForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.IPBlocklist
|
||||
fields = ["address"]
|
||||
|
||||
|
||||
class ServerForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.FederatedServer
|
||||
exclude = ["remote_id"]
|
||||
|
||||
|
||||
class AutoModRuleForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.AutoMod
|
||||
fields = ["string_match", "flag_users", "flag_statuses", "created_by"]
|
||||
|
||||
|
||||
class IntervalScheduleForm(CustomForm):
|
||||
class Meta:
|
||||
model = IntervalSchedule
|
||||
fields = ["every", "period"]
|
||||
|
||||
widgets = {
|
||||
"every": forms.NumberInput(attrs={"aria-describedby": "desc_every"}),
|
||||
"period": forms.Select(attrs={"aria-describedby": "desc_period"}),
|
||||
}
|
47
bookwyrm/forms/author.py
Normal file
47
bookwyrm/forms/author.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
""" using django model forms """
|
||||
from django import forms
|
||||
|
||||
from bookwyrm import models
|
||||
from .custom_form import CustomForm
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
class AuthorForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Author
|
||||
fields = [
|
||||
"last_edited_by",
|
||||
"name",
|
||||
"aliases",
|
||||
"bio",
|
||||
"wikipedia_link",
|
||||
"born",
|
||||
"died",
|
||||
"openlibrary_key",
|
||||
"inventaire_id",
|
||||
"librarything_key",
|
||||
"goodreads_key",
|
||||
"isni",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
||||
"aliases": forms.TextInput(attrs={"aria-describedby": "desc_aliases"}),
|
||||
"bio": forms.Textarea(attrs={"aria-describedby": "desc_bio"}),
|
||||
"wikipedia_link": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_wikipedia_link"}
|
||||
),
|
||||
"born": forms.SelectDateWidget(attrs={"aria-describedby": "desc_born"}),
|
||||
"died": forms.SelectDateWidget(attrs={"aria-describedby": "desc_died"}),
|
||||
"oepnlibrary_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_oepnlibrary_key"}
|
||||
),
|
||||
"inventaire_id": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_inventaire_id"}
|
||||
),
|
||||
"librarything_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_librarything_key"}
|
||||
),
|
||||
"goodreads_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_goodreads_key"}
|
||||
),
|
||||
}
|
111
bookwyrm/forms/books.py
Normal file
111
bookwyrm/forms/books.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
""" using django model forms """
|
||||
from django import forms
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models.fields import ClearableFileInputWithWarning
|
||||
from .custom_form import CustomForm
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
class CoverForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Book
|
||||
fields = ["cover"]
|
||||
help_texts = {f: None for f in fields}
|
||||
|
||||
|
||||
class ArrayWidget(forms.widgets.TextInput):
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=no-self-use
|
||||
def value_from_datadict(self, data, files, name):
|
||||
"""get all values for this name"""
|
||||
return [i for i in data.getlist(name) if i]
|
||||
|
||||
|
||||
class EditionForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Edition
|
||||
exclude = [
|
||||
"remote_id",
|
||||
"origin_id",
|
||||
"created_date",
|
||||
"updated_date",
|
||||
"edition_rank",
|
||||
"authors",
|
||||
"parent_work",
|
||||
"shelves",
|
||||
"connector",
|
||||
"search_vector",
|
||||
"links",
|
||||
"file_links",
|
||||
]
|
||||
widgets = {
|
||||
"title": forms.TextInput(attrs={"aria-describedby": "desc_title"}),
|
||||
"subtitle": forms.TextInput(attrs={"aria-describedby": "desc_subtitle"}),
|
||||
"description": forms.Textarea(
|
||||
attrs={"aria-describedby": "desc_description"}
|
||||
),
|
||||
"series": forms.TextInput(attrs={"aria-describedby": "desc_series"}),
|
||||
"series_number": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_series_number"}
|
||||
),
|
||||
"subjects": ArrayWidget(),
|
||||
"languages": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_languages_help desc_languages"}
|
||||
),
|
||||
"publishers": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
|
||||
),
|
||||
"first_published_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_first_published_date"}
|
||||
),
|
||||
"published_date": forms.SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_published_date"}
|
||||
),
|
||||
"cover": ClearableFileInputWithWarning(
|
||||
attrs={"aria-describedby": "desc_cover"}
|
||||
),
|
||||
"physical_format": forms.Select(
|
||||
attrs={"aria-describedby": "desc_physical_format"}
|
||||
),
|
||||
"physical_format_detail": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_physical_format_detail"}
|
||||
),
|
||||
"pages": forms.NumberInput(attrs={"aria-describedby": "desc_pages"}),
|
||||
"isbn_13": forms.TextInput(attrs={"aria-describedby": "desc_isbn_13"}),
|
||||
"isbn_10": forms.TextInput(attrs={"aria-describedby": "desc_isbn_10"}),
|
||||
"openlibrary_key": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_openlibrary_key"}
|
||||
),
|
||||
"inventaire_id": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_inventaire_id"}
|
||||
),
|
||||
"oclc_number": forms.TextInput(
|
||||
attrs={"aria-describedby": "desc_oclc_number"}
|
||||
),
|
||||
"ASIN": forms.TextInput(attrs={"aria-describedby": "desc_ASIN"}),
|
||||
}
|
||||
|
||||
|
||||
class EditionFromWorkForm(CustomForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# make all fields hidden
|
||||
for visible in self.visible_fields():
|
||||
visible.field.widget = forms.HiddenInput()
|
||||
|
||||
class Meta:
|
||||
model = models.Work
|
||||
fields = [
|
||||
"title",
|
||||
"subtitle",
|
||||
"authors",
|
||||
"description",
|
||||
"languages",
|
||||
"series",
|
||||
"series_number",
|
||||
"subjects",
|
||||
"subject_places",
|
||||
"cover",
|
||||
"first_published_date",
|
||||
]
|
26
bookwyrm/forms/custom_form.py
Normal file
26
bookwyrm/forms/custom_form.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
""" Overrides django's default form class """
|
||||
from collections import defaultdict
|
||||
from django.forms import ModelForm
|
||||
from django.forms.widgets import Textarea
|
||||
|
||||
|
||||
class CustomForm(ModelForm):
|
||||
"""add css classes to the forms"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
css_classes = defaultdict(lambda: "")
|
||||
css_classes["text"] = "input"
|
||||
css_classes["password"] = "input"
|
||||
css_classes["email"] = "input"
|
||||
css_classes["number"] = "input"
|
||||
css_classes["checkbox"] = "checkbox"
|
||||
css_classes["textarea"] = "textarea"
|
||||
# pylint: disable=super-with-arguments
|
||||
super(CustomForm, self).__init__(*args, **kwargs)
|
||||
for visible in self.visible_fields():
|
||||
if hasattr(visible.field.widget, "input_type"):
|
||||
input_type = visible.field.widget.input_type
|
||||
if isinstance(visible.field.widget, Textarea):
|
||||
input_type = "textarea"
|
||||
visible.field.widget.attrs["rows"] = 5
|
||||
visible.field.widget.attrs["class"] = css_classes[input_type]
|
68
bookwyrm/forms/edit_user.py
Normal file
68
bookwyrm/forms/edit_user.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
""" using django model forms """
|
||||
from django import forms
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models.fields import ClearableFileInputWithWarning
|
||||
from .custom_form import CustomForm
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
class EditUserForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = [
|
||||
"avatar",
|
||||
"name",
|
||||
"email",
|
||||
"summary",
|
||||
"show_goal",
|
||||
"show_suggested_users",
|
||||
"manually_approves_followers",
|
||||
"default_post_privacy",
|
||||
"discoverable",
|
||||
"hide_follows",
|
||||
"preferred_timezone",
|
||||
"preferred_language",
|
||||
"theme",
|
||||
]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
"avatar": ClearableFileInputWithWarning(
|
||||
attrs={"aria-describedby": "desc_avatar"}
|
||||
),
|
||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
||||
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
|
||||
"email": forms.EmailInput(attrs={"aria-describedby": "desc_email"}),
|
||||
"discoverable": forms.CheckboxInput(
|
||||
attrs={"aria-describedby": "desc_discoverable"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class LimitedEditUserForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = [
|
||||
"avatar",
|
||||
"name",
|
||||
"summary",
|
||||
"manually_approves_followers",
|
||||
"discoverable",
|
||||
]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
"avatar": ClearableFileInputWithWarning(
|
||||
attrs={"aria-describedby": "desc_avatar"}
|
||||
),
|
||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
||||
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
|
||||
"discoverable": forms.CheckboxInput(
|
||||
attrs={"aria-describedby": "desc_discoverable"}
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class DeleteUserForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["password"]
|
59
bookwyrm/forms/forms.py
Normal file
59
bookwyrm/forms/forms.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
""" using django model forms """
|
||||
from django import forms
|
||||
from django.forms import widgets
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models.user import FeedFilterChoices
|
||||
from .custom_form import CustomForm
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
class FeedStatusTypesForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["feed_status_types"]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
"feed_status_types": widgets.CheckboxSelectMultiple(
|
||||
choices=FeedFilterChoices,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class ImportForm(forms.Form):
|
||||
csv_file = forms.FileField()
|
||||
|
||||
|
||||
class ShelfForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Shelf
|
||||
fields = ["user", "name", "privacy", "description"]
|
||||
|
||||
|
||||
class GoalForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.AnnualGoal
|
||||
fields = ["user", "year", "goal", "privacy"]
|
||||
|
||||
|
||||
class ReportForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Report
|
||||
fields = ["user", "reporter", "status", "links", "note"]
|
||||
|
||||
|
||||
class ReadThroughForm(CustomForm):
|
||||
def clean(self):
|
||||
"""make sure the email isn't in use by a registered user"""
|
||||
cleaned_data = super().clean()
|
||||
start_date = cleaned_data.get("start_date")
|
||||
finish_date = cleaned_data.get("finish_date")
|
||||
if start_date and finish_date and start_date > finish_date:
|
||||
self.add_error(
|
||||
"finish_date", _("Reading finish date cannot be before start date.")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.ReadThrough
|
||||
fields = ["user", "book", "start_date", "finish_date"]
|
16
bookwyrm/forms/groups.py
Normal file
16
bookwyrm/forms/groups.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
""" using django model forms """
|
||||
from bookwyrm import models
|
||||
from .custom_form import CustomForm
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
class UserGroupForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["groups"]
|
||||
|
||||
|
||||
class GroupForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Group
|
||||
fields = ["user", "privacy", "name", "description"]
|
45
bookwyrm/forms/landing.py
Normal file
45
bookwyrm/forms/landing.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
""" Forms for the landing pages """
|
||||
from django.forms import PasswordInput
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from bookwyrm import models
|
||||
from .custom_form import CustomForm
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
class LoginForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["localname", "password"]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
"password": PasswordInput(),
|
||||
}
|
||||
|
||||
|
||||
class RegisterForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["localname", "email", "password"]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {"password": PasswordInput()}
|
||||
|
||||
def clean(self):
|
||||
"""Check if the username is taken"""
|
||||
cleaned_data = super().clean()
|
||||
localname = cleaned_data.get("localname").strip()
|
||||
if models.User.objects.filter(localname=localname).first():
|
||||
self.add_error("localname", _("User with this username already exists"))
|
||||
|
||||
|
||||
class InviteRequestForm(CustomForm):
|
||||
def clean(self):
|
||||
"""make sure the email isn't in use by a registered user"""
|
||||
cleaned_data = super().clean()
|
||||
email = cleaned_data.get("email")
|
||||
if email and models.User.objects.filter(email=email).exists():
|
||||
self.add_error("email", _("A user with this email already exists."))
|
||||
|
||||
class Meta:
|
||||
model = models.InviteRequest
|
||||
fields = ["email"]
|
48
bookwyrm/forms/links.py
Normal file
48
bookwyrm/forms/links.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
""" using django model forms """
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from bookwyrm import models
|
||||
from .custom_form import CustomForm
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
class LinkDomainForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.LinkDomain
|
||||
fields = ["name"]
|
||||
|
||||
|
||||
class FileLinkForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.FileLink
|
||||
fields = ["url", "filetype", "availability", "book", "added_by"]
|
||||
|
||||
def clean(self):
|
||||
"""make sure the domain isn't blocked or pending"""
|
||||
cleaned_data = super().clean()
|
||||
url = cleaned_data.get("url")
|
||||
filetype = cleaned_data.get("filetype")
|
||||
book = cleaned_data.get("book")
|
||||
domain = urlparse(url).netloc
|
||||
if models.LinkDomain.objects.filter(domain=domain).exists():
|
||||
status = models.LinkDomain.objects.get(domain=domain).status
|
||||
if status == "blocked":
|
||||
# pylint: disable=line-too-long
|
||||
self.add_error(
|
||||
"url",
|
||||
_(
|
||||
"This domain is blocked. Please contact your administrator if you think this is an error."
|
||||
),
|
||||
)
|
||||
elif models.FileLink.objects.filter(
|
||||
url=url, book=book, filetype=filetype
|
||||
).exists():
|
||||
# pylint: disable=line-too-long
|
||||
self.add_error(
|
||||
"url",
|
||||
_(
|
||||
"This link with file type has already been added for this book. If it is not visible, the domain is still pending."
|
||||
),
|
||||
)
|
37
bookwyrm/forms/lists.py
Normal file
37
bookwyrm/forms/lists.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
""" using django model forms """
|
||||
from django import forms
|
||||
from django.forms import ChoiceField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from bookwyrm import models
|
||||
from .custom_form import CustomForm
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
class ListForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.List
|
||||
fields = ["user", "name", "description", "curation", "privacy", "group"]
|
||||
|
||||
|
||||
class ListItemForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.ListItem
|
||||
fields = ["user", "book", "book_list", "notes"]
|
||||
|
||||
|
||||
class SortListForm(forms.Form):
|
||||
sort_by = ChoiceField(
|
||||
choices=(
|
||||
("order", _("List Order")),
|
||||
("title", _("Book Title")),
|
||||
("rating", _("Rating")),
|
||||
),
|
||||
label=_("Sort By"),
|
||||
)
|
||||
direction = ChoiceField(
|
||||
choices=(
|
||||
("ascending", _("Ascending")),
|
||||
("descending", _("Descending")),
|
||||
),
|
||||
)
|
82
bookwyrm/forms/status.py
Normal file
82
bookwyrm/forms/status.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
""" using django model forms """
|
||||
from bookwyrm import models
|
||||
from .custom_form import CustomForm
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
class RatingForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.ReviewRating
|
||||
fields = ["user", "book", "rating", "privacy"]
|
||||
|
||||
|
||||
class ReviewForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Review
|
||||
fields = [
|
||||
"user",
|
||||
"book",
|
||||
"name",
|
||||
"content",
|
||||
"rating",
|
||||
"content_warning",
|
||||
"sensitive",
|
||||
"privacy",
|
||||
]
|
||||
|
||||
|
||||
class CommentForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Comment
|
||||
fields = [
|
||||
"user",
|
||||
"book",
|
||||
"content",
|
||||
"content_warning",
|
||||
"sensitive",
|
||||
"privacy",
|
||||
"progress",
|
||||
"progress_mode",
|
||||
"reading_status",
|
||||
]
|
||||
|
||||
|
||||
class QuotationForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Quotation
|
||||
fields = [
|
||||
"user",
|
||||
"book",
|
||||
"quote",
|
||||
"content",
|
||||
"content_warning",
|
||||
"sensitive",
|
||||
"privacy",
|
||||
"position",
|
||||
"position_mode",
|
||||
]
|
||||
|
||||
|
||||
class ReplyForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Status
|
||||
fields = [
|
||||
"user",
|
||||
"content",
|
||||
"content_warning",
|
||||
"sensitive",
|
||||
"reply_parent",
|
||||
"privacy",
|
||||
]
|
||||
|
||||
|
||||
class StatusForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Status
|
||||
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
|
||||
|
||||
|
||||
class DirectForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Status
|
||||
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
|
54
bookwyrm/management/commands/instance_version.py
Normal file
54
bookwyrm/management/commands/instance_version.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
""" Get your admin code to allow install """
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.settings import VERSION
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class Command(BaseCommand):
|
||||
"""command-line options"""
|
||||
|
||||
help = "What version is this?"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""specify which function to run"""
|
||||
parser.add_argument(
|
||||
"--current",
|
||||
action="store_true",
|
||||
help="Version stored in database",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target",
|
||||
action="store_true",
|
||||
help="Version stored in settings",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--update",
|
||||
action="store_true",
|
||||
help="Update database version",
|
||||
)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def handle(self, *args, **options):
|
||||
"""execute init"""
|
||||
site = models.SiteSettings.objects.get()
|
||||
current = site.version or "0.0.1"
|
||||
target = VERSION
|
||||
if options.get("current"):
|
||||
print(current)
|
||||
return
|
||||
|
||||
if options.get("target"):
|
||||
print(target)
|
||||
return
|
||||
|
||||
if options.get("update"):
|
||||
site.version = target
|
||||
site.save()
|
||||
return
|
||||
|
||||
if current != target:
|
||||
print(f"{current}/{target}")
|
||||
else:
|
||||
print(current)
|
68
bookwyrm/migrations/0142_auto_20220227_1752.py
Normal file
68
bookwyrm/migrations/0142_auto_20220227_1752.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
# Generated by Django 3.2.12 on 2022-02-27 17:52
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def add_default_themes(apps, schema_editor):
|
||||
"""add light and dark themes"""
|
||||
db_alias = schema_editor.connection.alias
|
||||
theme_model = apps.get_model("bookwyrm", "Theme")
|
||||
theme_model.objects.using(db_alias).create(
|
||||
name="BookWyrm Light",
|
||||
path="css/themes/bookwyrm-light.scss",
|
||||
)
|
||||
theme_model.objects.using(db_alias).create(
|
||||
name="BookWyrm Dark",
|
||||
path="css/themes/bookwyrm-dark.scss",
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0141_alter_report_status"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Theme",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||
("name", models.CharField(max_length=50, unique=True)),
|
||||
("path", models.CharField(max_length=50, unique=True)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="sitesettings",
|
||||
name="default_theme",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="bookwyrm.theme",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="theme",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="bookwyrm.theme",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
add_default_themes, reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
19
bookwyrm/migrations/0142_user_hide_follows.py
Normal file
19
bookwyrm/migrations/0142_user_hide_follows.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 3.2.12 on 2022-02-28 19:44
|
||||
|
||||
import bookwyrm.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0141_alter_report_status"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="hide_follows",
|
||||
field=bookwyrm.models.fields.BooleanField(default=False),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,13 @@
|
|||
# Generated by Django 3.2.12 on 2022-02-28 21:28
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0142_auto_20220227_1752"),
|
||||
("bookwyrm", "0142_user_hide_follows"),
|
||||
]
|
||||
|
||||
operations = []
|
39
bookwyrm/migrations/0144_alter_announcement_display_type.py
Normal file
39
bookwyrm/migrations/0144_alter_announcement_display_type.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
# Generated by Django 3.2.12 on 2022-03-01 18:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def remove_white(apps, schema_editor):
|
||||
"""don't hardcode white announcements"""
|
||||
db_alias = schema_editor.connection.alias
|
||||
announcement_model = apps.get_model("bookwyrm", "Announcement")
|
||||
announcement_model.objects.using(db_alias).filter(display_type="white-ter").update(
|
||||
display_type=None
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0143_merge_0142_auto_20220227_1752_0142_user_hide_follows"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="announcement",
|
||||
name="display_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("primary-light", "Primary"),
|
||||
("success-light", "Success"),
|
||||
("link-light", "Link"),
|
||||
("warning-light", "Warning"),
|
||||
("danger-light", "Danger"),
|
||||
],
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(remove_white, reverse_code=migrations.RunPython.noop),
|
||||
]
|
18
bookwyrm/migrations/0145_sitesettings_version.py
Normal file
18
bookwyrm/migrations/0145_sitesettings_version.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.12 on 2022-03-16 18:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0144_alter_announcement_display_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="sitesettings",
|
||||
name="version",
|
||||
field=models.CharField(blank=True, max_length=10, null=True),
|
||||
),
|
||||
]
|
|
@ -26,7 +26,7 @@ from .group import Group, GroupMember, GroupMemberInvitation
|
|||
|
||||
from .import_job import ImportJob, ImportItem
|
||||
|
||||
from .site import SiteSettings, SiteInvite
|
||||
from .site import SiteSettings, Theme, SiteInvite
|
||||
from .site import PasswordReset, InviteRequest
|
||||
from .announcement import Announcement
|
||||
from .antispam import EmailBlocklist, IPBlocklist, AutoMod, automod_task
|
||||
|
|
|
@ -8,7 +8,6 @@ from .base_model import BookWyrmModel
|
|||
|
||||
|
||||
DisplayTypes = [
|
||||
("white-ter", _("None")),
|
||||
("primary-light", _("Primary")),
|
||||
("success-light", _("Success")),
|
||||
("link-light", _("Link")),
|
||||
|
@ -28,11 +27,7 @@ class Announcement(BookWyrmModel):
|
|||
end_date = models.DateTimeField(blank=True, null=True)
|
||||
active = models.BooleanField(default=True)
|
||||
display_type = models.CharField(
|
||||
max_length=20,
|
||||
blank=False,
|
||||
null=False,
|
||||
choices=DisplayTypes,
|
||||
default="white-ter",
|
||||
max_length=20, choices=DisplayTypes, null=True, blank=True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -24,6 +24,10 @@ class SiteSettings(models.Model):
|
|||
)
|
||||
instance_description = models.TextField(default="This instance has no description.")
|
||||
instance_short_description = models.CharField(max_length=255, blank=True, null=True)
|
||||
default_theme = models.ForeignKey(
|
||||
"Theme", null=True, blank=True, on_delete=models.SET_NULL
|
||||
)
|
||||
version = models.CharField(null=True, blank=True, max_length=10)
|
||||
|
||||
# admin setup options
|
||||
install_mode = models.BooleanField(default=False)
|
||||
|
@ -104,6 +108,18 @@ class SiteSettings(models.Model):
|
|||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Theme(models.Model):
|
||||
"""Theme files"""
|
||||
|
||||
created_date = models.DateTimeField(auto_now_add=True)
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
path = models.CharField(max_length=50, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
# pylint: disable=invalid-str-returned
|
||||
return self.name
|
||||
|
||||
|
||||
class SiteInvite(models.Model):
|
||||
"""gives someone access to create an account on the instance"""
|
||||
|
||||
|
|
|
@ -227,7 +227,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
|||
@classmethod
|
||||
def privacy_filter(cls, viewer, privacy_levels=None):
|
||||
queryset = super().privacy_filter(viewer, privacy_levels=privacy_levels)
|
||||
return queryset.filter(deleted=False)
|
||||
return queryset.filter(deleted=False, user__is_active=True)
|
||||
|
||||
@classmethod
|
||||
def direct_filter(cls, queryset, viewer):
|
||||
|
|
|
@ -136,6 +136,8 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
updated_date = models.DateTimeField(auto_now=True)
|
||||
last_active_date = models.DateTimeField(default=timezone.now)
|
||||
manually_approves_followers = fields.BooleanField(default=False)
|
||||
theme = models.ForeignKey("Theme", null=True, blank=True, on_delete=models.SET_NULL)
|
||||
hide_follows = fields.BooleanField(default=False)
|
||||
|
||||
# options to turn features on and off
|
||||
show_goal = models.BooleanField(default=True)
|
||||
|
@ -478,10 +480,13 @@ def set_remote_server(user_id):
|
|||
get_remote_reviews.delay(user.outbox)
|
||||
|
||||
|
||||
def get_or_create_remote_server(domain):
|
||||
def get_or_create_remote_server(domain, refresh=False):
|
||||
"""get info on a remote server"""
|
||||
server = FederatedServer()
|
||||
try:
|
||||
return FederatedServer.objects.get(server_name=domain)
|
||||
server = FederatedServer.objects.get(server_name=domain)
|
||||
if not refresh:
|
||||
return server
|
||||
except FederatedServer.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
@ -496,13 +501,15 @@ def get_or_create_remote_server(domain):
|
|||
application_type = data.get("software", {}).get("name")
|
||||
application_version = data.get("software", {}).get("version")
|
||||
except ConnectorException:
|
||||
if server.id:
|
||||
return server
|
||||
application_type = application_version = None
|
||||
|
||||
server = FederatedServer.objects.create(
|
||||
server_name=domain,
|
||||
application_type=application_type,
|
||||
application_version=application_version,
|
||||
)
|
||||
server.server_name = domain
|
||||
server.application_type = application_type
|
||||
server.application_version = application_version
|
||||
|
||||
server.save()
|
||||
return server
|
||||
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
env = Env()
|
||||
env.read_env()
|
||||
DOMAIN = env("DOMAIN")
|
||||
VERSION = "0.3.1"
|
||||
VERSION = "0.3.4"
|
||||
|
||||
RELEASE_API = env(
|
||||
"RELEASE_API",
|
||||
|
@ -21,7 +21,7 @@ RELEASE_API = env(
|
|||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||
|
||||
JS_CACHE = "a60e5a55"
|
||||
JS_CACHE = "bc93172a"
|
||||
|
||||
# email
|
||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||
|
@ -90,6 +90,7 @@ INSTALLED_APPS = [
|
|||
"sass_processor",
|
||||
"bookwyrm",
|
||||
"celery",
|
||||
"django_celery_beat",
|
||||
"imagekit",
|
||||
"storages",
|
||||
]
|
||||
|
@ -188,10 +189,7 @@ STATICFILES_FINDERS = [
|
|||
]
|
||||
|
||||
SASS_PROCESSOR_INCLUDE_FILE_PATTERN = r"^.+\.[s]{0,1}(?:a|c)ss$"
|
||||
|
||||
SASS_PROCESSOR_INCLUDE_DIRS = [
|
||||
os.path.join(BASE_DIR, ".css-config-sample"),
|
||||
]
|
||||
SASS_PROCESSOR_ENABLED = True
|
||||
|
||||
# minify css is production but not dev
|
||||
if not DEBUG:
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
@charset "utf-8";
|
||||
|
||||
@import "instance-settings";
|
||||
@import "themes/light.scss";
|
||||
@import "vendor/bulma/bulma.sass";
|
||||
@import "vendor/icons.css";
|
||||
@import "bookwyrm/all.scss";
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/** Imports
|
||||
******************************************************************************/
|
||||
@import "components/avatar";
|
||||
@import "components/barcode";
|
||||
@import "components/book_cover";
|
||||
@import "components/book_grid";
|
||||
@import "components/book_list";
|
||||
|
@ -115,7 +116,7 @@ button .button-invisible-overlay {
|
|||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background: rgba($scheme-invert, 0.66);
|
||||
background: $invisible-overlay-background-color;
|
||||
color: white;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
|
|
26
bookwyrm/static/css/bookwyrm/components/_barcode.scss
Normal file
26
bookwyrm/static/css/bookwyrm/components/_barcode.scss
Normal file
|
@ -0,0 +1,26 @@
|
|||
/* Barcode scanner CSS */
|
||||
#barcode-scanner {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
text-align: center;
|
||||
height: calc(70vh - 200px);
|
||||
|
||||
video {
|
||||
height: calc(70vh - 200px);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
height: calc(70vh - 200px);
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#barcode-camera-list {
|
||||
float: right;
|
||||
}
|
|
@ -53,7 +53,7 @@ details.dropdown .dropdown-menu a:focus-visible {
|
|||
|
||||
@media only screen and (max-width: 768px) {
|
||||
details.dropdown[open] summary.dropdown-trigger::before {
|
||||
background-color: rgba($scheme-invert, 0.5);
|
||||
background-color: $modal-background-background-color;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
.toggle-button[aria-pressed="true"],
|
||||
.toggle-button[aria-pressed="true"]:hover {
|
||||
background-color: hsl(171deg, 100%, 41%);
|
||||
background-color: $primary;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
|
@ -39,6 +39,7 @@
|
|||
<glyph unicode="" glyph-name="graphic-heart" d="M934.176 791.52c-116.128 115.072-301.824 117.472-422.112 9.216-120.32 108.256-305.952 105.856-422.144-9.216-119.712-118.528-119.712-310.688 0-429.28 34.208-33.888 353.696-350.112 353.696-350.112 37.856-37.504 99.072-37.504 136.896 0 0 0 349.824 346.304 353.696 350.112 119.744 118.592 119.744 310.752-0.032 429.28zM888.576 407.424l-353.696-350.112c-12.576-12.512-33.088-12.512-45.6 0l-353.696 350.112c-94.4 93.44-94.4 245.472 0 338.912 91.008 90.080 237.312 93.248 333.088 7.104l43.392-39.040 43.36 39.040c95.808 86.144 242.112 83.008 333.12-7.104 94.4-93.408 94.4-245.44 0.032-338.912zM296.096 719.968c8.864 0 16-7.168 16-16s-7.168-16-16-16h-0.032c-57.408 0-103.968-46.56-103.968-103.968v-0.032c0-8.832-7.168-16-16-16s-16 7.168-16 16v0c0 75.072 60.832 135.904 135.872 135.968 0.064 0 0.064 0.032 0.128 0.032z" />
|
||||
<glyph unicode="" glyph-name="graphic-paperplane" d="M1009.376 954.88c-5.312 3.424-11.36 5.12-17.376 5.12-6.176 0-12.384-1.76-17.76-5.376l-960-640c-9.888-6.56-15.328-18.112-14.048-29.952 1.216-11.808 8.896-22.016 19.936-26.368l250.368-100.192 117.728-206.016c5.632-9.888 16.096-16 27.424-16.128 0.128 0 0.224 0 0.352 0 11.232 0 21.664 5.952 27.424 15.552l66.464 110.816 310.24-124.064c3.808-1.536 7.808-2.272 11.872-2.272 5.44 0 10.816 1.376 15.68 4.128 8.448 4.736 14.24 13.056 15.872 22.624l160 960c2.080 12.576-3.488 25.184-14.176 32.128zM100.352 295.136l741.6 494.432-539.2-577.184c-2.848 1.696-5.376 3.936-8.512 5.184l-193.888 77.568zM326.048 189.888c-0.064 0.128-0.16 0.192-0.224 0.32l606.176 648.8-516.768-805.184-89.184 156.064zM806.944 12.512l-273.312 109.312c-6.496 2.56-13.248 3.424-19.936 3.808l420.864 652.416-127.616-765.536z" />
|
||||
<glyph unicode="" glyph-name="graphic-banknote" d="M1005.28 621.248l-320 320c-15.872 15.872-38.88 22.24-60.672 16.864-11.488-2.816-21.76-8.736-29.888-16.864-7.264-7.264-12.736-16.256-15.872-26.304-14.496-47.008-39.552-87.872-76.64-124.928-49.536-49.504-114.048-87.008-182.304-126.656-72.448-41.984-147.296-85.504-208.64-146.816-52.128-52.192-87.616-110.24-108.416-177.632-7.008-22.752-0.896-47.36 15.872-64.192l320-320c15.872-15.872 38.88-22.24 60.672-16.864 11.488 2.88 21.76 8.736 29.888 16.864 7.264 7.264 12.736 16.256 15.872 26.368 14.528 47.008 39.584 87.872 76.704 124.928 49.504 49.504 113.984 86.944 182.304 126.56 72.384 42.048 147.264 85.568 208.576 146.88 52.128 52.128 87.616 110.24 108.448 177.632 6.976 22.72 0.832 47.424-15.904 64.16zM384 0c-105.984 105.984-214.016 214.048-320 320 90.944 294.432 485.12 281.568 576 576 105.984-105.952 214.048-214.016 320.064-320-90.976-294.368-485.152-281.568-576.064-576zM625.984 483.2c-10.432 8.736-20.928 14.688-31.488 17.632-10.496 2.944-20.992 4.128-31.616 3.36-10.496-0.8-21.248-3.2-32-7.328-10.752-4.192-21.568-8.736-32.448-14.016-17.184 19.744-34.368 39.264-51.552 57.376 7.744 7.008 15.264 10.56 22.496 10.816 7.264 0.32 14.24-0.448 20.864-2.112 6.752-1.696 12.928-3.136 18.624-4.256 5.76-1.12 10.752 0.128 15.136 3.808 4.64 4 7.2 9.184 7.552 15.424 0.32 6.304-2.048 12.448-7.328 18.432-6.752 7.744-14.88 12.448-24.64 14.176-9.632 1.696-19.488 1.568-29.76-0.672-10.112-2.304-19.744-6.112-28.864-11.488s-16.448-10.88-21.888-16.256c-2.080 1.984-4.16 3.936-6.24 5.888-2.304 2.112-5.184 3.264-8.64 3.2-3.488 0-6.368-1.504-8.736-4.256-2.304-2.688-3.36-5.824-2.944-9.12 0.32-3.424 1.696-6.048 4.064-8.064 2.080-1.76 4.16-3.488 6.24-5.312-8.192-9.888-14.944-20.8-20.256-32.32-5.376-11.552-8.576-23.008-9.76-34.112-1.248-11.2-0.064-21.44 3.36-30.944 3.424-9.568 9.76-17.696 19.008-25.376 15.072-12.512 32.8-17.824 53.376-16.64 20.512 1.248 42.624 7.36 66.4 20.128 18.88-21.824 37.824-43.488 56.736-63.616-8-6.752-15.008-10.624-21.184-11.872-6.176-1.312-11.68-1.184-16.672 0.32-4.992 1.568-9.632 3.808-13.888 6.688-4.256 2.944-8.448 5.44-12.64 7.488-4.128 2.048-8.384 3.2-12.736 3.264s-8.992-2.048-14.112-6.432c-5.248-4.576-7.872-9.888-7.872-15.872 0-5.952 2.752-12 8.128-18.112 5.44-6.112 12.512-11.264 21.056-15.328s18.208-6.624 28.832-7.328c10.624-0.736 21.824 0.864 33.632 5.248 11.872 4.32 23.616 12.128 35.2 23.744 5.568-5.44 11.2-10.624 16.8-15.616 2.368-2.048 5.248-3.072 8.736-2.816 3.36 0.128 6.304 1.696 8.64 4.512 2.368 2.88 3.36 6.048 3.008 9.376-0.32 3.36-1.696 5.952-4 7.808-5.632 4.512-11.264 9.248-16.864 14.24 9.568 11.744 17.248 24.128 22.944 36.384 5.696 12.32 9.056 24.192 10.176 35.2 1.12 11.072-0.192 21.056-3.808 30.112-3.584 9.184-9.952 17.056-19.072 24.64zM447.072 461.504c-9.056-0.384-16.96 2.624-23.872 9.312-2.944 2.816-4.992 6.24-6.24 10.304-1.312 4.064-1.76 8.512-1.248 13.376 0.448 4.8 1.888 9.824 4.384 14.88 2.368 5.056 5.888 10.112 10.368 15.008 16.224-16.128 32.416-33.824 48.64-52.128-12.288-6.752-22.976-10.368-32.032-10.752zM598.016 397.44c-2.88-5.312-6.176-10.048-10.048-14.176-17.952 18.112-35.872 38.016-53.76 58.432 4.576 2.048 9.376 4.192 14.56 6.368s10.368 3.616 15.552 4.512c5.312 0.8 10.56 0.576 15.808-0.672 5.184-1.312 10.112-4.128 14.688-8.576 4.512-4.512 7.36-9.184 8.512-14.24 1.248-5.12 1.312-10.304 0.448-15.616-0.928-5.344-2.816-10.656-5.76-16.032zM470.944 250.24c6.304 5.088 15.584 4.832 21.376-1.056 6.272-6.24 6.272-16.448 0-22.688-0.512-0.512-1.056-0.864-1.632-1.312l0.064-0.064c-20.256-15.392-36.896-29.248-54.848-47.2-16.224-16.192-30.88-33.248-43.552-50.56l-20.448-28c-0.64-1.152-1.408-2.208-2.368-3.2-6.272-6.24-16.48-6.24-22.72 0-5.44 5.44-6.112 13.824-2.112 20.064l-0.064 0.064 21.888 29.888c13.664 18.688 29.376 36.992 46.752 54.368 18.080 18.144 37.6 34.336 57.6 49.696h0.064zM588.096 713.12c16.192 16.192 30.816 33.184 43.52 50.592l21.248 29.12c0.768 1.376 1.632 2.752 2.816 3.936 6.304 6.304 16.512 6.304 22.816 0 5.984-6.016 6.24-15.52 0.8-21.888l0.064-0.064-21.888-30.016c-13.696-18.688-29.376-36.928-46.752-54.304-18.080-18.080-37.568-34.336-57.568-49.696l-0.128 0.064c-6.368-5.856-16.256-5.728-22.368 0.448-6.304 6.304-6.304 16.576 0 22.88 1.12 1.184 2.432 2.016 3.744 2.752 18.816 14.368 36.96 29.44 53.696 46.176z" />
|
||||
<glyph unicode="" glyph-name="barcode" d="M0 832h128v-640h-128zM192 832h64v-640h-64zM320 832h64v-640h-64zM512 832h64v-640h-64zM768 832h64v-640h-64zM960 832h64v-640h-64zM640 832h32v-640h-32zM448 832h32v-640h-32zM864 832h32v-640h-32zM0 128h64v-64h-64zM192 128h64v-64h-64zM320 128h64v-64h-64zM640 128h64v-64h-64zM960 128h64v-64h-64zM768 128h128v-64h-128zM448 128h128v-64h-128z" />
|
||||
<glyph unicode="" glyph-name="spinner" d="M384 832c0 70.692 57.308 128 128 128s128-57.308 128-128c0-70.692-57.308-128-128-128s-128 57.308-128 128zM655.53 719.53c0 70.692 57.308 128 128 128s128-57.308 128-128c0-70.692-57.308-128-128-128s-128 57.308-128 128zM832 448c0 35.346 28.654 64 64 64s64-28.654 64-64c0-35.346-28.654-64-64-64s-64 28.654-64 64zM719.53 176.47c0 35.346 28.654 64 64 64s64-28.654 64-64c0-35.346-28.654-64-64-64s-64 28.654-64 64zM448.002 64c0 0 0 0 0 0 0 35.346 28.654 64 64 64s64-28.654 64-64c0 0 0 0 0 0 0-35.346-28.654-64-64-64s-64 28.654-64 64zM176.472 176.47c0 0 0 0 0 0 0 35.346 28.654 64 64 64s64-28.654 64-64c0 0 0 0 0 0 0-35.346-28.654-64-64-64s-64 28.654-64 64zM144.472 719.53c0 0 0 0 0 0 0 53.019 42.981 96 96 96s96-42.981 96-96c0 0 0 0 0 0 0-53.019-42.981-96-96-96s-96 42.981-96 96zM56 448c0 39.765 32.235 72 72 72s72-32.235 72-72c0-39.765-32.235-72-72-72s-72 32.235-72 72z" />
|
||||
<glyph unicode="" glyph-name="search" d="M992.262 88.604l-242.552 206.294c-25.074 22.566-51.89 32.926-73.552 31.926 57.256 67.068 91.842 154.078 91.842 249.176 0 212.078-171.922 384-384 384-212.076 0-384-171.922-384-384s171.922-384 384-384c95.098 0 182.108 34.586 249.176 91.844-1-21.662 9.36-48.478 31.926-73.552l206.294-242.552c35.322-39.246 93.022-42.554 128.22-7.356s31.892 92.898-7.354 128.22zM384 320c-141.384 0-256 114.616-256 256s114.616 256 256 256 256-114.616 256-256-114.614-256-256-256z" />
|
||||
<glyph unicode="" glyph-name="star-empty" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538zM512 206.502l-223.462-117.48 42.676 248.83-180.786 176.222 249.84 36.304 111.732 226.396 111.736-226.396 249.836-36.304-180.788-176.222 42.678-248.83-223.462 117.48z" />
|
||||
|
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Binary file not shown.
Binary file not shown.
84
bookwyrm/static/css/themes/bookwyrm-dark.scss
Normal file
84
bookwyrm/static/css/themes/bookwyrm-dark.scss
Normal file
|
@ -0,0 +1,84 @@
|
|||
@import "../vendor/bulma/sass/utilities/initial-variables.sass";
|
||||
|
||||
/* Colors
|
||||
******************************************************************************/
|
||||
|
||||
/* states */
|
||||
$primary: #005e50;
|
||||
$primary-light: #1d2b28;
|
||||
$info: #1f4666;
|
||||
$success: #246447;
|
||||
$warning: #8b6c15;
|
||||
$danger: #872538;
|
||||
$danger-light: #481922;
|
||||
$light: #393939;
|
||||
$red: #ffa1b4;
|
||||
|
||||
/* book cover standins */
|
||||
$no-cover-color: #002549;
|
||||
|
||||
/* background colors */
|
||||
$scheme-main: rgb(24, 27, 28);
|
||||
$scheme-invert: #fff;
|
||||
$scheme-main-bis: rgb(28, 30, 32);
|
||||
$scheme-main-ter: rgb(32, 34, 36);
|
||||
$background-body: rgb(24, 27, 28);
|
||||
$background-secondary: rgb(28, 30, 32);
|
||||
$background-tertiary: rgb(32, 34, 36);
|
||||
$modal-background-background-color: rgba($black, 0.8);
|
||||
|
||||
/* highlight colors */
|
||||
$primary-highlight: $primary;
|
||||
$info-highlight: $info;
|
||||
$success-highlight: $success;
|
||||
|
||||
/* borders */
|
||||
$border: #2b3031;
|
||||
$border-light: #444;
|
||||
$border-hover: #51595d;
|
||||
|
||||
/* text */
|
||||
$text: $grey-lightest;
|
||||
$text-light: $grey-lighter;
|
||||
$text-strong: $white-ter;
|
||||
|
||||
/* links */
|
||||
$link: #2e7eb9;
|
||||
$link-background: $background-tertiary;
|
||||
$link-hover: $white-bis;
|
||||
$link-hover-border: #51595d;
|
||||
$link-focus: $white-bis;
|
||||
$link-active: $white-bis;
|
||||
|
||||
/* bulma overrides */
|
||||
$background: $background-secondary;
|
||||
$menu-item-active-background-color: $link-background;
|
||||
$navbar-dropdown-item-hover-color: $white;
|
||||
|
||||
/* These element's colors are hardcoded, probably a bug in bulma? */
|
||||
@media screen and (min-width: 769px) {
|
||||
.navbar-dropdown {
|
||||
box-shadow: 0 8px 8px rgba($black, 0.2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.navbar-menu {
|
||||
box-shadow: 0 8px 8px rgba($black, 0.2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* misc */
|
||||
$shadow: 0 0.5em 1em -0.125em rgba($black, 0.2), 0 0px 0 1px rgba($black, 0.02);
|
||||
$card-header-shadow: 0 0.125em 0.25em rgba($black, 0.1);
|
||||
$invisible-overlay-background-color: rgba($black, 0.66);
|
||||
$progress-value-background-color: $border-light;
|
||||
|
||||
/* Fonts
|
||||
******************************************************************************/
|
||||
$family-primary: $family-sans-serif;
|
||||
$family-secondary: $family-sans-serif;
|
||||
|
||||
|
||||
@import "../bookwyrm.scss";
|
||||
@import "../vendor/icons.css";
|
|
@ -47,7 +47,13 @@ $link-active: $grey-darker;
|
|||
$background: $background-secondary;
|
||||
$menu-item-active-background-color: $link-background;
|
||||
|
||||
/* misc */
|
||||
$invisible-overlay-background-color: rgba($scheme-invert, 0.66);
|
||||
|
||||
/* Fonts
|
||||
******************************************************************************/
|
||||
$family-primary: $family-sans-serif;
|
||||
$family-secondary: $family-sans-serif;
|
||||
|
||||
@import "../bookwyrm.scss";
|
||||
@import "../vendor/icons.css";
|
|
@ -1,55 +0,0 @@
|
|||
@import "../vendor/bulma/sass/utilities/derived-variables.sass";
|
||||
|
||||
/* Colors
|
||||
******************************************************************************/
|
||||
|
||||
/* states */
|
||||
$primary: #016a5b;
|
||||
$info: #1f4666;
|
||||
$success: #246447;
|
||||
$warning: #8b6c15;
|
||||
$danger: #872538;
|
||||
|
||||
/* book cover standins */
|
||||
$no-cover-color: #002549;
|
||||
|
||||
/* background colors */
|
||||
$scheme-main: $grey-darker;
|
||||
$scheme-main-bis: $black-ter;
|
||||
$background-body: $grey-darker;
|
||||
$background-secondary: $grey-dark;
|
||||
$background-tertiary: #555;
|
||||
|
||||
/* highlight colors */
|
||||
$primary-highlight: $primary;
|
||||
$info-highlight: $info;
|
||||
$success-highlight: $success;
|
||||
|
||||
/* borders */
|
||||
$border: $grey;
|
||||
$border-hover: $grey-light;
|
||||
$border-light: $grey;
|
||||
$border-light-hover: $grey-light;
|
||||
|
||||
/* text */
|
||||
$text: $grey-lightest;
|
||||
$text-light: $grey-lighter;
|
||||
$text-strong: $white-ter;
|
||||
|
||||
/* links */
|
||||
$link: $white;
|
||||
$link-background: $background-tertiary;
|
||||
$link-hover: $white-bis;
|
||||
$link-focus: $white-bis;
|
||||
$link-active: $white-bis;
|
||||
|
||||
/* misc */
|
||||
|
||||
/* bulma overrides */
|
||||
$background: $background-secondary;
|
||||
$menu-item-active-background-color: $link-background;
|
||||
|
||||
/* Fonts
|
||||
******************************************************************************/
|
||||
$family-primary: $family-sans-serif;
|
||||
$family-secondary: $family-sans-serif;
|
3
bookwyrm/static/css/vendor/icons.css
vendored
3
bookwyrm/static/css/vendor/icons.css
vendored
|
@ -149,3 +149,6 @@
|
|||
.icon-download:before {
|
||||
content: "\ea36";
|
||||
}
|
||||
.icon-barcode:before {
|
||||
content: "\e937";
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* exported BookWyrm */
|
||||
/* globals TabGroup */
|
||||
/* globals TabGroup, Quagga */
|
||||
|
||||
let BookWyrm = new (class {
|
||||
constructor() {
|
||||
|
@ -38,15 +38,15 @@ let BookWyrm = new (class {
|
|||
.querySelectorAll("[data-modal-open]")
|
||||
.forEach((node) => node.addEventListener("click", this.handleModalButton.bind(this)));
|
||||
|
||||
document
|
||||
.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))
|
||||
);
|
||||
|
||||
document
|
||||
.querySelector("#barcode-scanner-modal")
|
||||
.addEventListener("open", this.openBarcodeScanner.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -427,9 +427,11 @@ let BookWyrm = new (class {
|
|||
});
|
||||
|
||||
modalElement.addEventListener("keydown", handleFocusTrap);
|
||||
modalElement.dispatchEvent(new Event("open"));
|
||||
}
|
||||
|
||||
function handleModalClose(modalElement) {
|
||||
modalElement.dispatchEvent(new Event("close"));
|
||||
modalElement.removeEventListener("keydown", handleFocusTrap);
|
||||
htmlElement.classList.remove("is-clipped");
|
||||
modalElement.classList.remove("is-active");
|
||||
|
@ -489,26 +491,6 @@ let BookWyrm = new (class {
|
|||
window.open(url, windowName, "left=100,top=100,width=430,height=600");
|
||||
}
|
||||
|
||||
duplicateInput(event) {
|
||||
const trigger = event.currentTarget;
|
||||
const input_id = trigger.dataset.duplicate;
|
||||
const orig = document.getElementById(input_id);
|
||||
const parent = orig.parentNode;
|
||||
const new_count = parent.querySelectorAll("input").length + 1;
|
||||
|
||||
let input = orig.cloneNode();
|
||||
|
||||
input.id += "-" + new_count;
|
||||
input.value = "";
|
||||
|
||||
let label = parent.querySelector("label").cloneNode();
|
||||
|
||||
label.setAttribute("for", input.id);
|
||||
|
||||
parent.appendChild(label);
|
||||
parent.appendChild(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a "click-to-copy" component from a textarea element
|
||||
* with `data-copytext`, `data-copytext-label`, `data-copytext-success`
|
||||
|
@ -632,4 +614,174 @@ let BookWyrm = new (class {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
openBarcodeScanner(event) {
|
||||
const scannerNode = document.getElementById("barcode-scanner");
|
||||
const statusNode = document.getElementById("barcode-status");
|
||||
const cameraListNode = document.querySelector("#barcode-camera-list > select");
|
||||
|
||||
cameraListNode.addEventListener("change", onChangeCamera);
|
||||
|
||||
function onChangeCamera(event) {
|
||||
initBarcodes(event.target.value);
|
||||
}
|
||||
|
||||
function toggleStatus(status) {
|
||||
for (const child of statusNode.children) {
|
||||
BookWyrm.toggleContainer(child, !child.classList.contains(status));
|
||||
}
|
||||
}
|
||||
|
||||
function initBarcodes(cameraId = null) {
|
||||
toggleStatus("grant-access");
|
||||
|
||||
if (!cameraId) {
|
||||
cameraId = sessionStorage.getItem("preferredCam");
|
||||
} else {
|
||||
sessionStorage.setItem("preferredCam", cameraId);
|
||||
}
|
||||
|
||||
scannerNode.replaceChildren();
|
||||
Quagga.stop();
|
||||
Quagga.init(
|
||||
{
|
||||
inputStream: {
|
||||
name: "Live",
|
||||
type: "LiveStream",
|
||||
target: scannerNode,
|
||||
constraints: {
|
||||
facingMode: "environment",
|
||||
deviceId: cameraId,
|
||||
},
|
||||
},
|
||||
decoder: {
|
||||
readers: [
|
||||
"ean_reader",
|
||||
{
|
||||
format: "ean_reader",
|
||||
config: {
|
||||
supplements: ["ean_2_reader", "ean_5_reader"],
|
||||
},
|
||||
},
|
||||
],
|
||||
multiple: false,
|
||||
},
|
||||
},
|
||||
(err) => {
|
||||
if (err) {
|
||||
scannerNode.replaceChildren();
|
||||
console.log(err);
|
||||
toggleStatus("access-denied");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let activeId = null;
|
||||
const track = Quagga.CameraAccess.getActiveTrack();
|
||||
|
||||
if (track) {
|
||||
activeId = track.getSettings().deviceId;
|
||||
}
|
||||
|
||||
Quagga.CameraAccess.enumerateVideoDevices().then((devices) => {
|
||||
cameraListNode.replaceChildren();
|
||||
|
||||
for (const device of devices) {
|
||||
const child = document.createElement("option");
|
||||
|
||||
child.value = device.deviceId;
|
||||
child.innerText = device.label.slice(0, 30);
|
||||
|
||||
if (activeId === child.value) {
|
||||
child.selected = true;
|
||||
}
|
||||
|
||||
cameraListNode.appendChild(child);
|
||||
}
|
||||
});
|
||||
|
||||
toggleStatus("scanning");
|
||||
Quagga.start();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function cleanup(clearDrawing = true) {
|
||||
Quagga.stop();
|
||||
cameraListNode.removeEventListener("change", onChangeCamera);
|
||||
|
||||
if (clearDrawing) {
|
||||
scannerNode.replaceChildren();
|
||||
}
|
||||
}
|
||||
|
||||
Quagga.onProcessed((result) => {
|
||||
const drawingCtx = Quagga.canvas.ctx.overlay;
|
||||
const drawingCanvas = Quagga.canvas.dom.overlay;
|
||||
|
||||
if (result) {
|
||||
if (result.boxes) {
|
||||
drawingCtx.clearRect(
|
||||
0,
|
||||
0,
|
||||
parseInt(drawingCanvas.getAttribute("width")),
|
||||
parseInt(drawingCanvas.getAttribute("height"))
|
||||
);
|
||||
result.boxes
|
||||
.filter((box) => box !== result.box)
|
||||
.forEach((box) => {
|
||||
Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, drawingCtx, {
|
||||
color: "green",
|
||||
lineWidth: 2,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (result.box) {
|
||||
Quagga.ImageDebug.drawPath(result.box, { x: 0, y: 1 }, drawingCtx, {
|
||||
color: "#00F",
|
||||
lineWidth: 2,
|
||||
});
|
||||
}
|
||||
|
||||
if (result.codeResult && result.codeResult.code) {
|
||||
Quagga.ImageDebug.drawPath(result.line, { x: "x", y: "y" }, drawingCtx, {
|
||||
color: "red",
|
||||
lineWidth: 3,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let lastDetection = null;
|
||||
let numDetected = 0;
|
||||
|
||||
Quagga.onDetected((result) => {
|
||||
// Detect the same code 3 times as an extra check to avoid bogus scans.
|
||||
if (lastDetection === null || lastDetection !== result.codeResult.code) {
|
||||
numDetected = 1;
|
||||
lastDetection = result.codeResult.code;
|
||||
|
||||
return;
|
||||
} else if (numDetected++ < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
const code = result.codeResult.code;
|
||||
|
||||
statusNode.querySelector(".isbn").innerText = code;
|
||||
toggleStatus("found");
|
||||
|
||||
const search = new URL("/search", document.location);
|
||||
|
||||
search.searchParams.set("q", code);
|
||||
|
||||
cleanup(false);
|
||||
location.assign(search);
|
||||
});
|
||||
|
||||
event.target.addEventListener("close", cleanup, { once: true });
|
||||
|
||||
initBarcodes();
|
||||
}
|
||||
})();
|
||||
|
|
49
bookwyrm/static/js/forms.js
Normal file
49
bookwyrm/static/js/forms.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
(function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Remoev input field
|
||||
*
|
||||
* @param {event} the button click event
|
||||
*/
|
||||
function removeInput(event) {
|
||||
const trigger = event.currentTarget;
|
||||
const input_id = trigger.dataset.remove;
|
||||
const input = document.getElementById(input_id);
|
||||
|
||||
input.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate an input field
|
||||
*
|
||||
* @param {event} the click even on the associated button
|
||||
*/
|
||||
function duplicateInput(event) {
|
||||
const trigger = event.currentTarget;
|
||||
const input_id = trigger.dataset.duplicate;
|
||||
const orig = document.getElementById(input_id);
|
||||
const parent = orig.parentNode;
|
||||
const new_count = parent.querySelectorAll("input").length + 1;
|
||||
|
||||
let input = orig.cloneNode();
|
||||
|
||||
input.id += "-" + new_count;
|
||||
input.value = "";
|
||||
|
||||
let label = parent.querySelector("label").cloneNode();
|
||||
|
||||
label.setAttribute("for", input.id);
|
||||
|
||||
parent.appendChild(label);
|
||||
parent.appendChild(input);
|
||||
}
|
||||
|
||||
document
|
||||
.querySelectorAll("[data-duplicate]")
|
||||
.forEach((node) => node.addEventListener("click", duplicateInput));
|
||||
|
||||
document
|
||||
.querySelectorAll("[data-remove]")
|
||||
.forEach((node) => node.addEventListener("click", removeInput));
|
||||
})();
|
|
@ -1,11 +1,11 @@
|
|||
/* exported TabGroup */
|
||||
|
||||
/*
|
||||
* The content below is licensed according to the W3C Software License at
|
||||
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
|
||||
* Heavily modified to web component by Zach Leatherman
|
||||
* Modified back to vanilla JavaScript with support for Bulma markup and nested tabs by Ned Zimmerman
|
||||
*/
|
||||
* The content below is licensed according to the W3C Software License at
|
||||
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
|
||||
* Heavily modified to web component by Zach Leatherman
|
||||
* Modified back to vanilla JavaScript with support for Bulma markup and nested tabs by Ned Zimmerman
|
||||
*/
|
||||
class TabGroup {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
|
@ -15,7 +15,7 @@ class TabGroup {
|
|||
this.panels = this.container.querySelectorAll(':scope > [role="tabpanel"]');
|
||||
this.delay = this.determineDelay();
|
||||
|
||||
if(!this.tablist || !this.tabs.length || !this.panels.length) {
|
||||
if (!this.tablist || !this.tabs.length || !this.panels.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ class TabGroup {
|
|||
left: 37,
|
||||
up: 38,
|
||||
right: 39,
|
||||
down: 40
|
||||
down: 40,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -42,23 +42,21 @@ class TabGroup {
|
|||
37: -1,
|
||||
38: -1,
|
||||
39: 1,
|
||||
40: 1
|
||||
40: 1,
|
||||
};
|
||||
}
|
||||
|
||||
initTabs() {
|
||||
let count = 0;
|
||||
for(let tab of this.tabs) {
|
||||
|
||||
for (let tab of this.tabs) {
|
||||
let isSelected = tab.getAttribute("aria-selected") === "true";
|
||||
|
||||
tab.setAttribute("tabindex", isSelected ? "0" : "-1");
|
||||
|
||||
tab.addEventListener('click', this.clickEventListener.bind(this));
|
||||
tab.addEventListener('keydown', this.keydownEventListener.bind(this));
|
||||
tab.addEventListener('keyup', this.keyupEventListener.bind(this));
|
||||
|
||||
if (isSelected) {
|
||||
tab.scrollIntoView();
|
||||
}
|
||||
tab.addEventListener("click", this.clickEventListener.bind(this));
|
||||
tab.addEventListener("keydown", this.keydownEventListener.bind(this));
|
||||
tab.addEventListener("keyup", this.keyupEventListener.bind(this));
|
||||
|
||||
tab.index = count++;
|
||||
}
|
||||
|
@ -68,8 +66,9 @@ class TabGroup {
|
|||
let selectedPanelId = this.tablist
|
||||
.querySelector('[role="tab"][aria-selected="true"]')
|
||||
.getAttribute("aria-controls");
|
||||
for(let panel of this.panels) {
|
||||
if(panel.getAttribute("id") !== selectedPanelId) {
|
||||
|
||||
for (let panel of this.panels) {
|
||||
if (panel.getAttribute("id") !== selectedPanelId) {
|
||||
panel.setAttribute("hidden", "");
|
||||
}
|
||||
panel.setAttribute("tabindex", "0");
|
||||
|
@ -86,16 +85,18 @@ class TabGroup {
|
|||
|
||||
// Handle keydown on tabs
|
||||
keydownEventListener(event) {
|
||||
var key = event.keyCode;
|
||||
const key = event.keyCode;
|
||||
|
||||
switch (key) {
|
||||
case this.keys.end:
|
||||
event.preventDefault();
|
||||
|
||||
// Activate last tab
|
||||
this.activateTab(this.tabs[this.tabs.length - 1]);
|
||||
break;
|
||||
case this.keys.home:
|
||||
event.preventDefault();
|
||||
|
||||
// Activate first tab
|
||||
this.activateTab(this.tabs[0]);
|
||||
break;
|
||||
|
@ -111,7 +112,7 @@ class TabGroup {
|
|||
|
||||
// Handle keyup on tabs
|
||||
keyupEventListener(event) {
|
||||
var key = event.keyCode;
|
||||
const key = event.keyCode;
|
||||
|
||||
switch (key) {
|
||||
case this.keys.left:
|
||||
|
@ -125,17 +126,16 @@ class TabGroup {
|
|||
// only up and down arrow should function.
|
||||
// In all other cases only left and right arrow function.
|
||||
determineOrientation(event) {
|
||||
var key = event.keyCode;
|
||||
var vertical = this.tablist.getAttribute('aria-orientation') == 'vertical';
|
||||
var proceed = false;
|
||||
const key = event.keyCode;
|
||||
const vertical = this.tablist.getAttribute("aria-orientation") == "vertical";
|
||||
let proceed = false;
|
||||
|
||||
if (vertical) {
|
||||
if (key === this.keys.up || key === this.keys.down) {
|
||||
event.preventDefault();
|
||||
proceed = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (key === this.keys.left || key === this.keys.right) {
|
||||
proceed = true;
|
||||
}
|
||||
|
@ -149,22 +149,21 @@ class TabGroup {
|
|||
// Either focus the next, previous, first, or last tab
|
||||
// depending on key pressed
|
||||
switchTabOnArrowPress(event) {
|
||||
var pressed = event.keyCode;
|
||||
const pressed = event.keyCode;
|
||||
|
||||
for (let tab of this.tabs) {
|
||||
tab.addEventListener('focus', this.focusEventHandler.bind(this));
|
||||
tab.addEventListener("focus", this.focusEventHandler.bind(this));
|
||||
}
|
||||
|
||||
if (this.direction[pressed]) {
|
||||
var target = event.target;
|
||||
const target = event.target;
|
||||
|
||||
if (target.index !== undefined) {
|
||||
if (this.tabs[target.index + this.direction[pressed]]) {
|
||||
this.tabs[target.index + this.direction[pressed]].focus();
|
||||
}
|
||||
else if (pressed === this.keys.left || pressed === this.keys.up) {
|
||||
} else if (pressed === this.keys.left || pressed === this.keys.up) {
|
||||
this.focusLastTab();
|
||||
}
|
||||
else if (pressed === this.keys.right || pressed == this.keys.down) {
|
||||
} else if (pressed === this.keys.right || pressed == this.keys.down) {
|
||||
this.focusFirstTab();
|
||||
}
|
||||
}
|
||||
|
@ -172,8 +171,8 @@ class TabGroup {
|
|||
}
|
||||
|
||||
// Activates any given tab panel
|
||||
activateTab (tab, setFocus) {
|
||||
if(tab.getAttribute("role") !== "tab") {
|
||||
activateTab(tab, setFocus) {
|
||||
if (tab.getAttribute("role") !== "tab") {
|
||||
tab = tab.closest('[role="tab"]');
|
||||
}
|
||||
|
||||
|
@ -183,19 +182,19 @@ class TabGroup {
|
|||
this.deactivateTabs();
|
||||
|
||||
// Remove tabindex attribute
|
||||
tab.removeAttribute('tabindex');
|
||||
tab.removeAttribute("tabindex");
|
||||
|
||||
// Set the tab as selected
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
tab.setAttribute("aria-selected", "true");
|
||||
|
||||
// Give the tab is-active class
|
||||
tab.classList.add('is-active');
|
||||
tab.classList.add("is-active");
|
||||
|
||||
// Get the value of aria-controls (which is an ID)
|
||||
var controls = tab.getAttribute('aria-controls');
|
||||
const controls = tab.getAttribute("aria-controls");
|
||||
|
||||
// Remove hidden attribute from tab panel to make it visible
|
||||
document.getElementById(controls).removeAttribute('hidden');
|
||||
document.getElementById(controls).removeAttribute("hidden");
|
||||
|
||||
// Set focus when required
|
||||
if (setFocus) {
|
||||
|
@ -206,14 +205,14 @@ class TabGroup {
|
|||
// Deactivate all tabs and tab panels
|
||||
deactivateTabs() {
|
||||
for (let tab of this.tabs) {
|
||||
tab.classList.remove('is-active');
|
||||
tab.setAttribute('tabindex', '-1');
|
||||
tab.setAttribute('aria-selected', 'false');
|
||||
tab.removeEventListener('focus', this.focusEventHandler.bind(this));
|
||||
tab.classList.remove("is-active");
|
||||
tab.setAttribute("tabindex", "-1");
|
||||
tab.setAttribute("aria-selected", "false");
|
||||
tab.removeEventListener("focus", this.focusEventHandler.bind(this));
|
||||
}
|
||||
|
||||
for (let panel of this.panels) {
|
||||
panel.setAttribute('hidden', 'hidden');
|
||||
panel.setAttribute("hidden", "hidden");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,15 +227,15 @@ class TabGroup {
|
|||
// Determine whether there should be a delay
|
||||
// when user navigates with the arrow keys
|
||||
determineDelay() {
|
||||
var hasDelay = this.tablist.hasAttribute('data-delay');
|
||||
var delay = 0;
|
||||
const hasDelay = this.tablist.hasAttribute("data-delay");
|
||||
let delay = 0;
|
||||
|
||||
if (hasDelay) {
|
||||
var delayValue = this.tablist.getAttribute('data-delay');
|
||||
const delayValue = this.tablist.getAttribute("data-delay");
|
||||
|
||||
if (delayValue) {
|
||||
delay = delayValue;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// If no value is specified, default to 300ms
|
||||
delay = 300;
|
||||
}
|
||||
|
@ -246,7 +245,7 @@ class TabGroup {
|
|||
}
|
||||
|
||||
focusEventHandler(event) {
|
||||
var target = event.target;
|
||||
const target = event.target;
|
||||
|
||||
setTimeout(this.checkTabFocus.bind(this), this.delay, target);
|
||||
}
|
3
bookwyrm/static/js/vendor/quagga.min.js
vendored
Normal file
3
bookwyrm/static/js/vendor/quagga.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -14,7 +14,8 @@
|
|||
{% cache 604800 about_page %}
|
||||
|
||||
{% get_book_superlatives as superlatives %}
|
||||
<section class="content pb-4">
|
||||
<section class=" pb-4">
|
||||
<div class="content">
|
||||
<h2>
|
||||
{% blocktrans with site_name=site.name %}Welcome to {{ site_name }}!{% endblocktrans %}
|
||||
</h2>
|
||||
|
@ -25,12 +26,13 @@
|
|||
While you can interact seamlessly with users anywhere in the <a href="https://joinbookwyrm.com/instances/" target="_blank">BookWyrm network</a>, this community is unique.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
{% if superlatives.top_rated %}
|
||||
{% with book=superlatives.top_rated.default_edition rating=superlatives.top_rated.rating %}
|
||||
<div class="column is-one-third is-flex">
|
||||
<div class="media notification">
|
||||
<div class="media notification is-clipped">
|
||||
<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' %}
|
||||
|
@ -49,7 +51,7 @@
|
|||
{% if superlatives.wanted %}
|
||||
{% with book=superlatives.wanted.default_edition %}
|
||||
<div class="column is-one-third is-flex">
|
||||
<div class="media notification">
|
||||
<div class="media notification is-clipped">
|
||||
<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' %}
|
||||
|
@ -68,7 +70,7 @@
|
|||
{% if superlatives.controversial %}
|
||||
{% with book=superlatives.controversial.default_edition %}
|
||||
<div class="column is-one-third is-flex">
|
||||
<div class="media notification">
|
||||
<div class="media notification is-clipped">
|
||||
<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' %}
|
||||
|
|
|
@ -19,8 +19,10 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button>
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<div class="buttons is-right is-flex-grow-1">
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
||||
|
|
|
@ -12,6 +12,15 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if update_error %}
|
||||
<div class="notification is-danger is-light">
|
||||
<span class="icon icon-x" aria-hidden="true"></span>
|
||||
<span>
|
||||
{% trans "Unable to connect to remote source." %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% with user_authenticated=request.user.is_authenticated can_edit_book=perms.bookwyrm.edit_book %}
|
||||
<div class="block" itemscope itemtype="https://schema.org/Book">
|
||||
<div class="columns is-mobile">
|
||||
|
@ -352,7 +361,7 @@
|
|||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if request.user.list_set.exists %}
|
||||
{% if list_options.exists %}
|
||||
<form name="list-add" method="post" action="{% url 'list-add-book' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
|
@ -361,7 +370,7 @@
|
|||
<div class="field has-addons">
|
||||
<div class="select control is-clipped">
|
||||
<select name="book_list" id="id_list">
|
||||
{% for list in user.list_set.all %}
|
||||
{% for list in list_options %}
|
||||
<option value="{{ list.id }}">{{ list.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
@ -386,6 +395,6 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{% static "js/vendor/tabs.js" %}?v={{ js_cache }}"></script>
|
||||
<script src="{% static "js/tabs.js" %}?v={{ js_cache }}"></script>
|
||||
<script src="{% static "js/autocomplete.js" %}?v={{ js_cache }}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -28,8 +28,10 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button class="button is-primary" type="submit">{% trans "Add" %}</button>
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<div class="buttons is-right is-flex-grow-1">
|
||||
<button class="button is-primary" type="submit">{% trans "Add" %}</button>
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="block">
|
||||
|
@ -79,6 +80,57 @@
|
|||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.languages.errors id="desc_languages" %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="id_add_subjects">
|
||||
{% trans "Subjects:" %}
|
||||
</label>
|
||||
{% for subject in book.subjects %}
|
||||
<label class="label is-sr-only" for="id_add_subject={% if not forloop.first %}-{{forloop.counter}}{% endif %}">
|
||||
{% trans "Add subject" %}
|
||||
</label>
|
||||
<div class="field has-addons" id="subject_field_wrapper_{{ forloop.counter }}">
|
||||
<div class="control is-expanded">
|
||||
<input
|
||||
id="id_add_subject-{{ forloop.counter }}"
|
||||
type="text"
|
||||
name="subjects"
|
||||
value="{{ subject }}"
|
||||
class="input"
|
||||
>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button
|
||||
class="button is-danger is-light"
|
||||
type="button"
|
||||
data-remove="subject_field_wrapper_{{ forloop.counter }}"
|
||||
>
|
||||
{% trans "Remove subject" as text %}
|
||||
<span class="icon icon-x" title="{{ text }}">
|
||||
<span class="is-sr-only">{{ text }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
name="subjects"
|
||||
id="id_add_subject"
|
||||
value="{{ subject }}"
|
||||
{% if confirm_mode %}readonly{% endif %}
|
||||
>
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.subjects.errors id="desc_subjects" %}
|
||||
</div>
|
||||
|
||||
<span class="help">
|
||||
<button class="button is-small" type="button" data-duplicate="id_add_subject" id="another_subject_field">
|
||||
<span class="icon icon-plus" aria-hidden="true"></span>
|
||||
<span>{% trans "Add Another Subject" %}</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
@ -153,7 +205,12 @@
|
|||
<input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'Jane Doe' %}" value="{{ author }}" {% if confirm_mode %}readonly{% endif %}>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="help"><button class="button is-small" type="button" data-duplicate="id_add_author" id="another_author_field">{% trans "Add Another Author" %}</button></span>
|
||||
<span class="help">
|
||||
<button class="button is-small" type="button" data-duplicate="id_add_author" id="another_author_field">
|
||||
<span class="icon icon-plus" aria-hidden="true"></span>
|
||||
<span>{% trans "Add Another Author" %}</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
@ -294,3 +351,7 @@
|
|||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{% static "js/forms.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -55,8 +55,10 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
|
||||
<div class="buttons is-right is-flex-grow-1">
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
||||
|
|
|
@ -17,13 +17,13 @@ Is that where you'd like to go?
|
|||
|
||||
|
||||
{% block modal-footer %}
|
||||
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer" class="button is-primary">{% trans "Continue" %}</a>
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="has-text-right is-flex-grow-1">
|
||||
<div class="is-flex-grow-1">
|
||||
<a href="{% url 'report-link' link.added_by.id link.id %}">{% trans "Report spam" %}</a>
|
||||
</div>
|
||||
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer" class="button is-primary">{% trans "Continue" %}</a>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -19,8 +19,10 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button>
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<div class="buttons is-right is-flex-grow-1">
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<button class="button is-primary" type="submit">{% trans "Confirm" %}</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
<footer class="card-footer">
|
||||
{% if user != request.user %}
|
||||
{% if user.mutuals %}
|
||||
{% if user.mutuals and not user.hide_follows %}
|
||||
<div class="card-footer-item">
|
||||
<div class="has-text-centered">
|
||||
<p class="title is-6 mb-0">{{ user.mutuals }}</p>
|
||||
|
|
|
@ -31,5 +31,5 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{% static "js/vendor/tabs.js" %}?v={{ js_cache }}"></script>
|
||||
<script src="{% static "js/tabs.js" %}?v={{ js_cache }}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block header %}
|
||||
{% trans "Create Group" %}
|
||||
{% trans "Create group" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
|
|
|
@ -8,13 +8,15 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<form name="delete-group-{{ group.id }}" action="{% url 'delete-group' group.id %}" method="POST">
|
||||
<form name="delete-group-{{ group.id }}" action="{% url 'delete-group' group.id %}" method="POST" class="is-flex-grow-1">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="id" value="{{ group.id }}">
|
||||
<div class="buttons is-right is-flex-grow-1">
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<button class="button is-danger" type="submit">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -16,12 +16,14 @@
|
|||
</div>
|
||||
<div class="is-flex">
|
||||
{% if group.id %}
|
||||
<div class="is-flex-grow-1">
|
||||
<div>
|
||||
<button type="button" data-modal-open="delete_group" class="button is-danger">
|
||||
{% trans "Delete group" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="is-flex is-flex-grow-1 is-justify-content-flex-end">
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
{% include 'snippets/privacy_select_no_followers.html' with current=group.privacy %}
|
||||
|
@ -30,4 +32,5 @@
|
|||
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
|
||||
</a>
|
||||
{% include 'snippets/add_to_group_button.html' with user=user group=group %}
|
||||
{% if user.mutuals %}
|
||||
{% if user.mutuals and not user.hide_follows %}
|
||||
<p class="help">
|
||||
{% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %}
|
||||
{{ mutuals }} follower you follow
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
{% load layout %}
|
||||
{% load sass_tags %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load sass_tags %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="{% get_lang %}">
|
||||
<head>
|
||||
<title>{% block title %}BookWyrm{% endblock %} - {{ site.name }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="{% sass_src 'css/bookwyrm.scss' %}" rel="stylesheet" type="text/css" />
|
||||
<link href="{% sass_src site_theme %}" rel="stylesheet" type="text/css" />
|
||||
|
||||
<link rel="search" type="application/opensearchdescription+xml" href="{% url 'opensearch' %}" title="{% blocktrans with site_name=site.name %}{{ site_name }} search{% endblocktrans %}" />
|
||||
|
||||
|
@ -56,8 +56,16 @@
|
|||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button" type="button" data-modal-open="barcode-scanner-modal">
|
||||
<span class="icon icon-barcode" title="{% trans 'Scan Barcode' %}">
|
||||
<span class="is-sr-only">{% trans "Scan Barcode" %}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% include "search/barcode_modal.html" with id="barcode-scanner-modal" %}
|
||||
|
||||
<button type="button" tabindex="0" class="navbar-burger pulldown-menu my-4" data-controls="main_nav" aria-expanded="false">
|
||||
<i class="icon icon-dots-three-vertical" aria-hidden="true"></i>
|
||||
|
@ -266,6 +274,7 @@
|
|||
<script src="{% static "js/bookwyrm.js" %}?v={{ js_cache }}"></script>
|
||||
<script src="{% static "js/localstorage.js" %}?v={{ js_cache }}"></script>
|
||||
<script src="{% static "js/status_cache.js" %}?v={{ js_cache }}"></script>
|
||||
<script src="{% static "js/vendor/quagga.min.js" %}?v={{ js_cache }}"></script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
|
|
|
@ -32,14 +32,16 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button type="submit" class="button is-link">
|
||||
<div class="buttons is-right is-flex-grow-1">
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<button type="submit" class="button is-link">
|
||||
{% if list.curation == 'open' or request.user == list.user or list.group|is_member:request.user %}
|
||||
{% trans "Add" %}
|
||||
{% else %}
|
||||
{% trans "Suggest" %}
|
||||
{% endif %}
|
||||
</button>
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
||||
|
|
|
@ -8,15 +8,17 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<form name="delete-list-{{ list.id }}" action="{% url 'delete-list' list.id %}" method="POST">
|
||||
<form name="delete-list-{{ list.id }}" action="{% url 'delete-list' list.id %}" method="POST" class="is-flex-grow-1">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="id" value="{{ list.id }}">
|
||||
<button class="button is-danger" type="submit">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
<div class="buttons is-right is-flex-grow-1">
|
||||
<button type="button" class="button" data-modal-close>
|
||||
{% trans "Cancel" %}
|
||||
</button>
|
||||
<button class="button is-danger" type="submit">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<div class="is-flex is-justify-content-end">
|
||||
{% if list.id %}
|
||||
<div class="is-flex-grow-1">
|
||||
<button type="button" data-modal-open="delete_list" class="button is-danger">
|
||||
|
|
|
@ -30,13 +30,23 @@
|
|||
|
||||
<div class="columns mt-3">
|
||||
<section class="column is-three-quarters">
|
||||
{% if request.GET.updated %}
|
||||
<div class="notification is-primary">
|
||||
{% if add_failed %}
|
||||
<div class="notification is-danger is-light">
|
||||
<span class="icon icon-x" aria-hidden="true"></span>
|
||||
<span>
|
||||
{% trans "That book is already on this list." %}
|
||||
</span>
|
||||
</div>
|
||||
{% elif add_succeeded %}
|
||||
<div class="notification is-success is-light">
|
||||
<span class="icon icon-check" aria-hidden="true"></span>
|
||||
<span>
|
||||
{% if list.curation != "open" and request.user != list.user and not list.group|is_member:request.user %}
|
||||
{% trans "You successfully suggested a book for this list!" %}
|
||||
{% else %}
|
||||
{% trans "You successfully added a book to this list!" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -107,7 +117,7 @@
|
|||
<summary>
|
||||
<span role="heading" aria-level="3">
|
||||
{% trans "Add notes" %}
|
||||
<span class="details-close icon icon-plus" aria-hidden="true"></span>
|
||||
<span class="details-close icon icon-x" aria-hidden="true"></span>
|
||||
</span>
|
||||
</summary>
|
||||
{% include "lists/edit_item_form.html" with book=item.book %}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
{% block profile-tabs %}
|
||||
<ul class="menu-list">
|
||||
<li><a href="#profile">{% trans "Profile" %}</a></li>
|
||||
<li><a href="#display-preferences">{% trans "Display preferences" %}</a></li>
|
||||
<li><a href="#display-preferences">{% trans "Display" %}</a></li>
|
||||
<li><a href="#privacy">{% trans "Privacy" %}</a></li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
@ -61,7 +61,7 @@
|
|||
<hr aria-hidden="true">
|
||||
|
||||
<section class="block" id="display-preferences">
|
||||
<h2 class="title is-4">{% trans "Display preferences" %}</h2>
|
||||
<h2 class="title is-4">{% trans "Display" %}</h2>
|
||||
<div class="box">
|
||||
<div class="field">
|
||||
<label class="checkbox label" for="id_show_goal">
|
||||
|
@ -97,6 +97,12 @@
|
|||
{{ form.preferred_language }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_them">{% trans "Theme:" %}</label>
|
||||
<div class="select">
|
||||
{{ form.theme }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
@ -111,6 +117,12 @@
|
|||
{% trans "Manually approve followers" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox label" for="id_hide_follows">
|
||||
{{ form.hide_follows }}
|
||||
{% trans "Hide followers and following on profile" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_default_post_privacy">
|
||||
{% trans "Default post privacy:" %}
|
||||
|
|
|
@ -14,12 +14,20 @@
|
|||
{% endblock %}
|
||||
|
||||
{% 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"
|
||||
class="is-flex-grow-1"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
||||
|
||||
<div class="buttons is-right">
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<button class="button is-danger" type="submit">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
@ -69,8 +69,10 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<div class="buttons is-right is-flex-grow-1">
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}
|
||||
|
|
48
bookwyrm/templates/search/barcode_modal.html
Normal file
48
bookwyrm/templates/search/barcode_modal.html
Normal file
|
@ -0,0 +1,48 @@
|
|||
{% extends 'components/modal.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-title %}
|
||||
{% blocktrans %}
|
||||
Scan Barcode
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="block">
|
||||
<div id="barcode-scanner"></div>
|
||||
</div>
|
||||
|
||||
<div id="barcode-camera-list" class="select is-small">
|
||||
<select>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="barcode-status" class="block">
|
||||
<div class="grant-access is-hidden">
|
||||
<span class="icon icon-lock"></span>
|
||||
<span class="is-size-5">{% trans "Requesting camera..." %}</span></br>
|
||||
<span>{% trans "Grant access to the camera to scan a book's barcode." %}</span>
|
||||
</div>
|
||||
<div class="access-denied is-hidden">
|
||||
<span class="icon icon-warning"></span>
|
||||
<span class="is-size-5">Access denied</span><br/>
|
||||
<span>{% trans "Could not access camera" %}</span>
|
||||
</div>
|
||||
<div class="scanning is-hidden">
|
||||
<span class="icon icon-barcode"></span>
|
||||
<span class="is-size-5">{% trans "Scanning..." context "barcode scanner" %}</span><br/>
|
||||
<span>{% trans "Align your book's barcode with the camera." %}</span>
|
||||
</div>
|
||||
<div class="found is-hidden">
|
||||
<span class="icon icon-check"></span>
|
||||
<span class="is-size-5">{% trans "ISBN scanned" context "barcode scanner" %}</span><br/>
|
||||
{% trans "Searching for book:" context "followed by ISBN" %} <span class="isbn"></span>...
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button class="button" type="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
{% endblock %}
|
||||
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
{% extends 'settings/layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block title %}
|
||||
|
@ -16,12 +17,81 @@
|
|||
<p>
|
||||
{% trans "Auto-moderation rules will create reports for any local user or status with fields matching the provided string." %}
|
||||
{% trans "Users or statuses that have already been reported (regardless of whether the report was resolved) will not be flagged." %}
|
||||
{% trans "At this time, reports are <em>not</em> being generated automatically, and you must manually trigger a scan." %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="box block">
|
||||
{% if task %}
|
||||
<dl class="block">
|
||||
<dt class="is-pulled-left mr-5 has-text-weight-bold">
|
||||
{% trans "Schedule:" %}
|
||||
</dt>
|
||||
<dd>
|
||||
{{ task.schedule }}
|
||||
</dd>
|
||||
|
||||
<dt class="is-pulled-left mr-5 has-text-weight-bold">
|
||||
{% trans "Last run:" %}
|
||||
</dt>
|
||||
<dd>
|
||||
{{ task.last_run_at|naturaltime }}
|
||||
</dd>
|
||||
|
||||
<dt class="is-pulled-left mr-5 has-text-weight-bold">
|
||||
{% trans "Total run count:" %}
|
||||
</dt>
|
||||
<dd>
|
||||
{{ task.total_run_count }}
|
||||
</dd>
|
||||
|
||||
<dt class="is-pulled-left mr-5 has-text-weight-bold">
|
||||
{% trans "Enabled:" %}
|
||||
</dt>
|
||||
<dd>
|
||||
<span class="tag {% if task.enabled %}is-success{% else %}is-danger{% endif %}">
|
||||
{{ task.enabled|yesno }}
|
||||
</span>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<div class="is-flex is-justify-content-space-between block">
|
||||
<form name="unschedule-scan" method="POST" action="{% url 'settings-automod-unschedule' task.id %}">
|
||||
{% csrf_token %}
|
||||
<button class="button is-danger">{% trans "Delete schedule" %}</button>
|
||||
</form>
|
||||
<form name="run-scan" method="POST" action="{% url 'settings-automod-run' %}">
|
||||
{% csrf_token %}
|
||||
<button class="button is-warning">{% trans "Run scan" %}</button>
|
||||
<button class="button">{% trans "Run now" %}</button>
|
||||
<p class="help">{% trans "Last run date will not be updated" %}</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<h2 class="title is-4">{% trans "Schedule scan" %}</h2>
|
||||
<form name="schedule-scan" method="POST" action="{% url 'settings-automod-schedule' %}">
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label class="label" for="id_every">
|
||||
{{ task_form.every.label }}
|
||||
</label>
|
||||
{{ task_form.every }}
|
||||
<p class="help" id="desc_every">
|
||||
{{ task_form.every.help_text }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_period">
|
||||
{{ task_form.period.label }}
|
||||
</label>
|
||||
<div class="select">
|
||||
{{ task_form.period }}
|
||||
</div>
|
||||
<p class="help" id="desc_period">
|
||||
{{ task_form.period.help_text }}
|
||||
</p>
|
||||
</div>
|
||||
<button class="button is-warning">{% trans "Schedule scan" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if success %}
|
||||
|
|
|
@ -4,7 +4,19 @@
|
|||
|
||||
{% block header %}
|
||||
{% trans "Add instance" %}
|
||||
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to instance list" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{% url 'settings-federation' %}">{% trans "Federated Instances" %}</a></li>
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">
|
||||
{% trans "Add instance" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
|
@ -73,9 +85,13 @@
|
|||
<label class="label" for="id_notes">
|
||||
{% trans "Notes:" %}
|
||||
</label>
|
||||
<textarea name="notes" cols="40" rows="5" class="textarea" id="id_notes">
|
||||
{{ form.notes.value|default:'' }}
|
||||
</textarea>
|
||||
<textarea
|
||||
name="notes"
|
||||
cols="40"
|
||||
rows="5"
|
||||
class="textarea"
|
||||
id="id_notes"
|
||||
>{{ form.notes.value|default:'' }}</textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button is-primary">
|
||||
|
|
|
@ -9,8 +9,26 @@
|
|||
|
||||
{% if server.status == "blocked" %}<span class="icon icon-x has-text-danger is-size-5" title="{% trans 'Blocked' %}"><span class="is-sr-only">{% trans "Blocked" %}</span></span>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to list" %}</a>
|
||||
{% block edit-button %}
|
||||
<form name="reload" method="POST" action="{% url 'settings-federated-server-refresh' server.id %}">
|
||||
{% csrf_token %}
|
||||
<button class="button" type="submit">{% trans "Refresh data" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{% url 'settings-federation' %}">{% trans "Federated Instances" %}</a></li>
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">
|
||||
{{ server.server_name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
|
|
|
@ -4,7 +4,19 @@
|
|||
|
||||
{% block header %}
|
||||
{% trans "Import Blocklist" %}
|
||||
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to instance list" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{% url 'settings-federation' %}">{% trans "Federated Instances" %}</a></li>
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">
|
||||
{% trans "Import Blocklist" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
|
|
|
@ -16,11 +16,11 @@
|
|||
<ul>
|
||||
{% url 'settings-federation' status='federated' as url %}
|
||||
<li {% if request.path in url %}class="is-active" aria-current="page"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Federated" %}</a>
|
||||
<a href="{{ url }}">{% trans "Federated" %} ({{ federated_count }})</a>
|
||||
</li>
|
||||
{% url 'settings-federation' status='blocked' as url %}
|
||||
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Blocked" %}</a>
|
||||
<a href="{{ url }}">{% trans "Blocked" %} ({{ blocked_count }})</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -86,6 +86,10 @@
|
|||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Site Settings" %}</a>
|
||||
{% block site-subtabs %}{% endblock %}
|
||||
</li>
|
||||
<li>
|
||||
{% url 'settings-themes' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Themes" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
|
|
@ -18,8 +18,10 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button type="submit" class="button is-primary">{% trans "Set" %}</button>
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<div class="buttons is-right is-flex-grow-1">
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<button type="submit" class="button is-primary">{% trans "Set" %}</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
||||
|
|
7
bookwyrm/templates/settings/reports/report_filters.html
Normal file
7
bookwyrm/templates/settings/reports/report_filters.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
||||
|
||||
{% block filter_fields %}
|
||||
{% include 'settings/users/username_filter.html' %}
|
||||
{% include 'directory/community_filter.html' %}
|
||||
{% include 'settings/users/server_filter.html' %}
|
||||
{% endblock %}
|
|
@ -30,7 +30,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
{% include 'settings/users/user_admin_filters.html' %}
|
||||
{% include 'settings/reports/report_filters.html' %}
|
||||
|
||||
<div class="block">
|
||||
{% if not reports %}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
{% block site-subtabs %}
|
||||
<ul class="menu-list">
|
||||
<li><a href="#instance-info">{% trans "Instance Info" %}</a></li>
|
||||
<li><a href="#images">{% trans "Images" %}</a></li>
|
||||
<li><a href="#display">{% trans "Display" %}</a></li>
|
||||
<li><a href="#footer">{% trans "Footer Content" %}</a></li>
|
||||
<li><a href="#registration">{% trans "Registration" %}</a></li>
|
||||
</ul>
|
||||
|
@ -33,7 +33,12 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="{% url 'settings-site' %}" method="POST" class="content" enctype="multipart/form-data">
|
||||
<form
|
||||
action="{% url 'settings-site' %}"
|
||||
method="POST"
|
||||
class="content"
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<section class="block" id="instance_info">
|
||||
<h2 class="title is-4">{% trans "Instance Info" %}</h2>
|
||||
|
@ -68,9 +73,11 @@
|
|||
|
||||
<hr aria-hidden="true">
|
||||
|
||||
<section class="block" id="images">
|
||||
<h2 class="title is-4">{% trans "Images" %}</h2>
|
||||
<div class="box is-flex">
|
||||
<section class="block" id="display">
|
||||
<h2 class="title is-4">{% trans "Display" %}</h2>
|
||||
<div class="box">
|
||||
<h3 class="title is-5">{% trans "Images" %}</h3>
|
||||
<div class="block is-flex">
|
||||
<div>
|
||||
<label class="label" for="id_logo">{% trans "Logo:" %}</label>
|
||||
{{ site_form.logo }}
|
||||
|
@ -84,6 +91,17 @@
|
|||
{{ site_form.favicon }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="title is-5">{% trans "Themes" %}</h3>
|
||||
<div class="block">
|
||||
<label class="label" for="id_default_theme">
|
||||
{% trans "Default theme:" %}
|
||||
</label>
|
||||
<div class="select">
|
||||
{{ site_form.default_theme }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr aria-hidden="true">
|
||||
|
|
122
bookwyrm/templates/settings/themes.html
Normal file
122
bookwyrm/templates/settings/themes.html
Normal file
|
@ -0,0 +1,122 @@
|
|||
{% extends 'settings/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Themes" %}{% endblock %}
|
||||
|
||||
{% block header %}{% trans "Themes" %}{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<a class="subtitle help is-link" href="{% url 'settings-site' %}/#display">
|
||||
{% trans "Set instance default theme" %}
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
{% if success %}
|
||||
<div class="notification is-success is-light">
|
||||
<span class="icon icon-check" aria-hidden="true"></span>
|
||||
<span>
|
||||
{% trans "Successfully added theme" %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<section class="block">
|
||||
<div class="notification content">
|
||||
<h2 class="title is-5">{% trans "How to add a theme" %}</h2>
|
||||
<ol>
|
||||
<li>
|
||||
{% trans "Copy the theme file into the <code>bookwyrm/static/css/themes</code> directory on your server from the command line." %}
|
||||
</li>
|
||||
<li>
|
||||
{% trans "Run <code>./bw-dev collectstatic</code>." %}
|
||||
</li>
|
||||
<li>
|
||||
{% trans "Add the file name using the form below to make it available in the application interface." %}
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block content">
|
||||
<h2 class="title is-4">{% trans "Add theme" %}</h2>
|
||||
|
||||
{% if theme_form.errors %}
|
||||
<div class="notification is-danger is-light">
|
||||
<span class="icon icon-x" aria-hidden="true"></span>
|
||||
<span>
|
||||
{% trans "Unable to save theme" %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="{% url 'settings-themes' %}"
|
||||
class="box"
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
<fieldset>
|
||||
{% csrf_token %}
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<label class="label" for="id_name">
|
||||
{% trans "Theme name" %}
|
||||
</label>
|
||||
<div class="control">
|
||||
{{ theme_form.name }}
|
||||
{% include 'snippets/form_errors.html' with errors_list=theme_form.name.errors id="desc_name" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<label class="label" for="id_path">
|
||||
{% trans "Theme filename" %}
|
||||
</label>
|
||||
<div class="control">
|
||||
{{ theme_form.path }}
|
||||
{% include 'snippets/form_errors.html' with errors_list=theme_form.path.errors id="desc_path" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button">{% trans "Add theme" %}</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="block content">
|
||||
<h2 class="title is-4">{% trans "Available Themes" %}</h2>
|
||||
<div class="table-container">
|
||||
<table class="table is-striped">
|
||||
<tr>
|
||||
<th>
|
||||
{% trans "Theme name" %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "File" %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Actions" %}
|
||||
</th>
|
||||
</tr>
|
||||
{% for theme in themes %}
|
||||
<tr>
|
||||
<td>{{ theme.name }}</td>
|
||||
<td><code>{{ theme.path }}</code></td>
|
||||
<td>
|
||||
<form method="POST" action="{% url 'settings-themes-delete' theme.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="button is-danger is-light is-small">
|
||||
<span class="icon icon-x" aria-hideen="true"></span>
|
||||
<span>{% trans "Remove theme" %}</span>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
16
bookwyrm/templates/settings/users/email_filter.html
Normal file
16
bookwyrm/templates/settings/users/email_filter.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% extends 'snippets/filters_panel/filter_field.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block filter %}
|
||||
<label class="label" for="id_email">{% trans "Email" %}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
name="email"
|
||||
value="{{ request.GET.email|default:'' }}"
|
||||
id="id_email" placeholder="user@email.com"
|
||||
>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,10 +1,23 @@
|
|||
{% extends 'settings/layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block title %}{{ user.username }}{% endblock %}
|
||||
{% block header %}
|
||||
{{ user.username }}
|
||||
<a class="help has-text-weight-normal" href="{% url 'settings-users' %}">{% trans "Back to users" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{% url 'settings-users' %}">{% trans "Users" %}</a></li>
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">
|
||||
{{ user|username }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{% extends 'settings/layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load utilities %}
|
||||
|
||||
{% block title %}{% trans "Users" %}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
|
@ -15,7 +17,23 @@
|
|||
|
||||
{% include 'settings/users/user_admin_filters.html' %}
|
||||
|
||||
<table class="table is-striped">
|
||||
<div class="block">
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
{% url 'settings-users' as url %}
|
||||
<li {% if request.path in url %}class="is-active" aria-current="page"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Local users" %}</a>
|
||||
</li>
|
||||
{% url 'settings-users' status="federated" as url %}
|
||||
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Federated community" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container block">
|
||||
<table class="table is-striped">
|
||||
<tr>
|
||||
{% url 'settings-users' as url %}
|
||||
<th>
|
||||
|
@ -34,27 +52,32 @@
|
|||
{% trans "Status" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="is_active" sort=sort text=text %}
|
||||
</th>
|
||||
{% if status != "local" %}
|
||||
<th>
|
||||
{% trans "Remote instance" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="federated_server__server_name" sort=sort text=text %}
|
||||
</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td><a href="{% url 'settings-user' user.id %}">{{ user.username }}</a></td>
|
||||
<td><a href="{% url 'settings-user' user.id %}">{{ user|username }}</a></td>
|
||||
<td>{{ user.created_date }}</td>
|
||||
<td>{{ user.last_active_date }}</td>
|
||||
<td>{% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}</td>
|
||||
{% if status != "local" %}
|
||||
<td>
|
||||
{% if user.federated_server %}
|
||||
<a href="{% url 'settings-federated-server' user.federated_server.id %}">{{ user.federated_server.server_name }}</a>
|
||||
{% elif not user.local %}
|
||||
{% else %}
|
||||
<em>{% trans "Not set" %}</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% include 'snippets/pagination.html' with page=users path=request.path %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
{% block filter_fields %}
|
||||
{% include 'settings/users/username_filter.html' %}
|
||||
{% include 'directory/community_filter.html' %}
|
||||
|
||||
{% if status != "local" %}
|
||||
{% include 'settings/users/server_filter.html' %}
|
||||
{% else %}
|
||||
{% include 'settings/users/email_filter.html' %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -71,14 +71,14 @@
|
|||
<dd>{{ user.last_active_date }}</dd>
|
||||
|
||||
<dt class="is-pulled-left mr-5">{% trans "Manually approved followers:" %}</dt>
|
||||
<dd>{{ user.manually_approves_followers }}</dd>
|
||||
<dd>{{ user.manually_approves_followers|yesno }}</dd>
|
||||
|
||||
<dt class="is-pulled-left mr-5">{% trans "Discoverable:" %}</dt>
|
||||
<dd>{{ user.discoverable }}</dd>
|
||||
<dd>{{ user.discoverable|yesno }}</dd>
|
||||
|
||||
{% if not user.is_active %}
|
||||
<dt class="is-pulled-left mr-5">{% trans "Deactivation reason:" %}</dt>
|
||||
<dd>{{ user.deactivation_reason }}</dd>
|
||||
<dd>{{ user.get_deactivation_reason_display }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if not user.is_active and user.deactivation_reason == "pending" %}
|
||||
|
@ -104,7 +104,7 @@
|
|||
<dd>{{ server.application_version }}</dd>
|
||||
|
||||
<dt class="is-pulled-left mr-5">{% trans "Status:" %}</dt>
|
||||
<dd>{{ server.status }}</dd>
|
||||
<dd>{{ server.get_status_display }}</dd>
|
||||
</dl>
|
||||
{% if server.notes %}
|
||||
<h5>{% trans "Notes" %}</h5>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block header %}
|
||||
{% trans "Create Shelf" %}
|
||||
{% trans "Create shelf" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<label class="label" for="id_description_{{ uuid }}">{% trans "Description:" %}</label>
|
||||
<textarea name="description" cols="40" rows="5" maxlength="500" class="textarea" id="id_description_{{ uuid }}">{{ form.description.value|default:'' }}</textarea>
|
||||
</div>
|
||||
<div class="field has-addons">
|
||||
<div class="field has-addons is-justify-content-end">
|
||||
<div class="control">
|
||||
{% include 'snippets/privacy_select.html' with current=privacy %}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% load humanize %}{% load i18n %}{% load utilities %}
|
||||
{% with announcement.id|uuid as uuid %}
|
||||
<aside
|
||||
class="notification mb-1 p-3{% if not admin_mode %} is-hidden{% endif %} transition-y has-background-{{ announcement.display_type }}"
|
||||
class="notification mb-1 p-3{% if not admin_mode %} is-hidden{% endif %} transition-y {% if announcement.display_type %}has-background-{{ announcement.display_type }}{% endif %}"
|
||||
{% if not admin_mode %}data-hide="hide_announcement_{{ announcement.id }}"{% endif %}
|
||||
>
|
||||
<details>
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
{% load i18n %}
|
||||
<div
|
||||
class="field{% if not reply_parent.content_warning and not draft.content_warning %} is-hidden{% endif %}"
|
||||
id="spoilers_{{ uuid }}{{ local_uuid }}"
|
||||
>
|
||||
<div class="field is-relative">
|
||||
<details
|
||||
{% if reply_parent.content_warning or draft.content_warning %}open{% endif %}
|
||||
>
|
||||
<summary class="is-flex">
|
||||
<span class="icon icon-warning is-size-5 mr-1" aria-hidden="true"></span>
|
||||
<span>
|
||||
{% trans "Include spoiler alert" %}
|
||||
</span>
|
||||
<span class="details-close icon icon-x" aria-hidden="true"></span>
|
||||
</summary>
|
||||
|
||||
<label
|
||||
class="label"
|
||||
for="id_content_warning_{{ uuid }}{{ local_uuid }}"
|
||||
>
|
||||
{% trans "Content warning:" %}
|
||||
{% trans "Spoilers/content warnings:" %}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
type="text"
|
||||
name="content_warning"
|
||||
|
@ -19,4 +28,6 @@
|
|||
value="{% firstof draft.content_warning reply_parent.content_warning '' %}"
|
||||
{% if not draft %}data-cache-draft="id_content_warning_{{ book.id }}_{{ type }}"{% endif %}
|
||||
>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
{% load i18n %}
|
||||
|
||||
<div class="control">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="is-hidden"
|
||||
name="sensitive"
|
||||
id="id_show_spoilers_{{ uuid }}{{ local_uuid }}"
|
||||
{% if draft.content_warning or status.content_warning %}checked{% endif %}
|
||||
aria-hidden="true"
|
||||
{% if not draft %}data-cache-draft="id_sensitive_{{ book.id }}_{{ type }}{{ reply_parent.id }}"{% endif %}
|
||||
>
|
||||
{% trans "Include spoiler alert" as button_text %}
|
||||
{% firstof draft.content_warning status.content_warning as pressed %}
|
||||
{% firstof local_uuid '' as local_uuid %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with text=button_text icon="warning is-size-4" controls_text="spoilers" controls_uid=uuid|add:local_uuid focus="id_content_warning" checkbox="id_show_spoilers" class="toggle-button" pressed=pressed %}
|
||||
</div>
|
|
@ -37,8 +37,6 @@ reply_parent: the Status object this post will be in reply to, if applicable
|
|||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% include "snippets/create_status/content_warning_field.html" %}
|
||||
|
||||
{# fields that go between the content warnings and the content field (ie, quote) #}
|
||||
{% block pre_content_additions %}{% endblock %}
|
||||
|
||||
|
@ -55,6 +53,8 @@ reply_parent: the Status object this post will be in reply to, if applicable
|
|||
{# additional fields that go after the content block (ie, progress) #}
|
||||
{% block post_content_additions %}{% endblock %}
|
||||
|
||||
{% include "snippets/create_status/content_warning_field.html" %}
|
||||
|
||||
{% block options_block %}
|
||||
{# cw, post privacy, and submit button #}
|
||||
{% include "snippets/create_status/post_options_block.html" %}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
{% load i18n %}
|
||||
<div class="columns mt-1">
|
||||
<div class="field has-addons column">
|
||||
{% include "snippets/create_status/content_warning_toggle.html" %}
|
||||
<div class="field has-addons is-justify-content-end">
|
||||
<div class="control">
|
||||
{% if type == 'direct' %}
|
||||
<input type="hidden" name="privacy" value="direct">
|
||||
|
@ -14,12 +12,10 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-narrow control">
|
||||
<div class="control">
|
||||
<button class="button is-link" type="submit">
|
||||
<span class="icon icon-spinner" aria-hidden="true"></span>
|
||||
<span>{% trans "Post" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit">
|
||||
{% trans "Sign Up" %}
|
||||
|
|
|
@ -48,10 +48,10 @@
|
|||
|
||||
|
||||
{% block modal-footer %}
|
||||
|
||||
<button class="button is-success" type="submit">{% trans "Submit" %}</button>
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
|
||||
<div class="buttons is-right is-flex-grow-1">
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<button class="button is-success" type="submit">{% trans "Submit" %}</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-form-close %}</form>{% endblock %}
|
||||
|
|
|
@ -71,7 +71,9 @@
|
|||
{% csrf_token %}
|
||||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
<input type="hidden" name="shelf" value="{{ user_shelf.id }}">
|
||||
<button class="button is-fullwidth is-small is-radiusless is-danger is-light" type="submit">{% trans "Remove from" %} {{ user_shelf.name }}</button>
|
||||
<button class="button is-fullwidth is-small is-radiusless is-danger is-light" type="submit">
|
||||
{% blocktrans with name=user_shelf|translate_shelf_name %}Remove from {{ name }}{% endblocktrans %}
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue