mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2025-02-27 16:26:37 +00:00
Merge branch 'main' into debug-toolbar
This commit is contained in:
commit
b0017094d5
389 changed files with 31604 additions and 11872 deletions
21
.env.example
21
.env.example
|
@ -41,7 +41,7 @@ REDIS_BROKER_PASSWORD=redispassword123
|
|||
|
||||
# Monitoring for celery
|
||||
FLOWER_PORT=8888
|
||||
FLOWER_USER=mouse
|
||||
FLOWER_USER=admin
|
||||
FLOWER_PASSWORD=changeme
|
||||
|
||||
# Email config
|
||||
|
@ -89,3 +89,22 @@ PREVIEW_TEXT_COLOR=#363636
|
|||
PREVIEW_IMG_WIDTH=1200
|
||||
PREVIEW_IMG_HEIGHT=630
|
||||
PREVIEW_DEFAULT_COVER_COLOR=#002549
|
||||
|
||||
# Below are example keys if you want to enable automatically
|
||||
# sending telemetry to an OTLP-compatible service. Many of
|
||||
# the main monitoring apps have OLTP collectors, including
|
||||
# NewRelic, DataDog, and Honeycomb.io - consult their
|
||||
# documentation for setup instructions, and what exactly to
|
||||
# put below!
|
||||
#
|
||||
# Service name is an arbitrary tag that is attached to any
|
||||
# data sent, used to distinguish different sources. Useful
|
||||
# for sending prod and dev metrics to the same place and
|
||||
# keeping them separate, for instance!
|
||||
|
||||
# API endpoint for your provider
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||
# Any headers required, usually authentication info
|
||||
OTEL_EXPORTER_OTLP_HEADERS=
|
||||
# Service name to identify your app
|
||||
OTEL_SERVICE_NAME=
|
||||
|
|
13
.github/workflows/lint-frontend.yaml
vendored
13
.github/workflows/lint-frontend.yaml
vendored
|
@ -1,5 +1,5 @@
|
|||
# @url https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||
name: Lint Frontend
|
||||
name: Lint Frontend (run `./bw-dev stylelint` to fix css errors)
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -8,7 +8,7 @@ on:
|
|||
- '.github/workflows/**'
|
||||
- 'static/**'
|
||||
- '.eslintrc'
|
||||
- '.stylelintrc'
|
||||
- '.stylelintrc.js'
|
||||
pull_request:
|
||||
branches: [ main, ci, frontend ]
|
||||
|
||||
|
@ -22,17 +22,16 @@ jobs:
|
|||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install modules
|
||||
run: yarn
|
||||
run: npm install stylelint stylelint-config-recommended stylelint-config-standard stylelint-order eslint
|
||||
|
||||
# See .stylelintignore for files that are not linted.
|
||||
- name: Run stylelint
|
||||
run: >
|
||||
yarn stylelint bookwyrm/static/**/*.css \
|
||||
--report-needless-disables \
|
||||
--report-invalid-scope-disables
|
||||
npx stylelint bookwyrm/static/css/*.scss bookwyrm/static/css/bookwyrm/**/*.scss \
|
||||
--config dev-tools/.stylelintrc.js
|
||||
|
||||
# See .eslintignore for files that are not linted.
|
||||
- name: Run ESLint
|
||||
run: >
|
||||
yarn eslint bookwyrm/static \
|
||||
npx eslint bookwyrm/static \
|
||||
--ext .js,.jsx,.ts,.tsx
|
||||
|
|
21
.github/workflows/lint-global.yaml
vendored
21
.github/workflows/lint-global.yaml
vendored
|
@ -1,21 +0,0 @@
|
|||
# @url https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||
name: Lint project globally
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, ci ]
|
||||
pull_request:
|
||||
branches: [ main, ci ]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint with EditorConfig.
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: EditorConfig
|
||||
uses: greut/eclint-action@v0
|
3
.github/workflows/prettier.yaml
vendored
3
.github/workflows/prettier.yaml
vendored
|
@ -17,8 +17,7 @@ jobs:
|
|||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install modules
|
||||
run: npm install .
|
||||
run: npm install prettier
|
||||
|
||||
# See .stylelintignore for files that are not linted.
|
||||
- name: Run Prettier
|
||||
run: npx prettier --check bookwyrm/static/js/*.js
|
||||
|
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -16,6 +16,9 @@
|
|||
# BookWyrm
|
||||
.env
|
||||
/images/
|
||||
bookwyrm/static/css/bookwyrm.css
|
||||
bookwyrm/static/css/themes/
|
||||
!bookwyrm/static/css/themes/bookwyrm-*.scss
|
||||
|
||||
# Testing
|
||||
.coverage
|
||||
|
@ -24,7 +27,9 @@
|
|||
.idea
|
||||
|
||||
#Node tools
|
||||
/node_modules/
|
||||
node_modules/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
#nginx
|
||||
nginx/default.conf
|
||||
|
|
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.
|
||||
|
||||
|
|
|
@ -227,7 +227,7 @@ def set_related_field(
|
|||
model_field = getattr(model, related_field_name)
|
||||
if hasattr(model_field, "activitypub_field"):
|
||||
setattr(activity, getattr(model_field, "activitypub_field"), instance.remote_id)
|
||||
item = activity.to_model()
|
||||
item = activity.to_model(model=model)
|
||||
|
||||
# if the related field isn't serialized (attachments on Status), then
|
||||
# we have to set it post-creation
|
||||
|
@ -298,6 +298,7 @@ class Link(ActivityObject):
|
|||
mediaType: str = None
|
||||
id: str = None
|
||||
attributedTo: str = None
|
||||
availability: str = None
|
||||
type: str = "Link"
|
||||
|
||||
def serialize(self, **kwargs):
|
||||
|
|
|
@ -16,6 +16,9 @@ class BookData(ActivityObject):
|
|||
librarythingKey: str = None
|
||||
goodreadsKey: str = None
|
||||
bnfId: str = None
|
||||
viaf: str = None
|
||||
wikidata: str = None
|
||||
asin: str = None
|
||||
lastEditedBy: str = None
|
||||
links: List[str] = field(default_factory=lambda: [])
|
||||
fileLinks: List[str] = field(default_factory=lambda: [])
|
||||
|
@ -27,8 +30,8 @@ class Book(BookData):
|
|||
"""serializes an edition or work, abstract"""
|
||||
|
||||
title: str
|
||||
sortTitle: str = ""
|
||||
subtitle: str = ""
|
||||
sortTitle: str = None
|
||||
subtitle: str = None
|
||||
description: str = ""
|
||||
languages: List[str] = field(default_factory=lambda: [])
|
||||
series: str = ""
|
||||
|
@ -53,7 +56,6 @@ class Edition(Book):
|
|||
isbn10: str = ""
|
||||
isbn13: str = ""
|
||||
oclcNumber: str = ""
|
||||
asin: str = ""
|
||||
pages: int = None
|
||||
physicalFormat: str = ""
|
||||
physicalFormatDetail: str = ""
|
||||
|
|
|
@ -39,4 +39,5 @@ class Person(ActivityObject):
|
|||
bookwyrmUser: bool = False
|
||||
manuallyApprovesFollowers: str = False
|
||||
discoverable: str = False
|
||||
hideFollows: str = False
|
||||
type: str = "Person"
|
||||
|
|
|
@ -38,7 +38,7 @@ class Create(Verb):
|
|||
class Delete(Verb):
|
||||
"""Create activity"""
|
||||
|
||||
to: List[str]
|
||||
to: List[str] = field(default_factory=lambda: [])
|
||||
cc: List[str] = field(default_factory=lambda: [])
|
||||
type: str = "Delete"
|
||||
|
||||
|
@ -137,8 +137,8 @@ class Accept(Verb):
|
|||
type: str = "Accept"
|
||||
|
||||
def action(self):
|
||||
"""find and remove the activity object"""
|
||||
obj = self.object.to_model(save=False, allow_create=False)
|
||||
"""accept a request"""
|
||||
obj = self.object.to_model(save=False, allow_create=True)
|
||||
obj.accept()
|
||||
|
||||
|
||||
|
@ -150,7 +150,7 @@ class Reject(Verb):
|
|||
type: str = "Reject"
|
||||
|
||||
def action(self):
|
||||
"""find and remove the activity object"""
|
||||
"""reject a follow request"""
|
||||
obj = self.object.to_model(save=False, allow_create=False)
|
||||
obj.reject()
|
||||
|
||||
|
|
54
bookwyrm/apps.py
Normal file
54
bookwyrm/apps.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
"""Do further startup configuration and initialization"""
|
||||
import os
|
||||
import urllib
|
||||
import logging
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
from bookwyrm import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def download_file(url, destination):
|
||||
"""Downloads a file to the given path"""
|
||||
try:
|
||||
# Ensure our destination directory exists
|
||||
os.makedirs(os.path.dirname(destination))
|
||||
with urllib.request.urlopen(url) as stream:
|
||||
with open(destination, "b+w") as outfile:
|
||||
outfile.write(stream.read())
|
||||
except (urllib.error.HTTPError, urllib.error.URLError):
|
||||
logger.info("Failed to download file %s", url)
|
||||
except OSError:
|
||||
logger.info("Couldn't open font file %s for writing", destination)
|
||||
except: # pylint: disable=bare-except
|
||||
logger.info("Unknown error in file download")
|
||||
|
||||
|
||||
class BookwyrmConfig(AppConfig):
|
||||
"""Handles additional configuration"""
|
||||
|
||||
name = "bookwyrm"
|
||||
verbose_name = "BookWyrm"
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def ready(self):
|
||||
"""set up OTLP and preview image files, if desired"""
|
||||
if settings.OTEL_EXPORTER_OTLP_ENDPOINT:
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from bookwyrm.telemetry import open_telemetry
|
||||
|
||||
open_telemetry.instrumentDjango()
|
||||
|
||||
if settings.ENABLE_PREVIEW_IMAGES and settings.FONTS:
|
||||
# Download any fonts that we don't have yet
|
||||
logger.debug("Downloading fonts..")
|
||||
for name, config in settings.FONTS.items():
|
||||
font_path = os.path.join(
|
||||
settings.FONT_DIR, config["directory"], config["filename"]
|
||||
)
|
||||
|
||||
if "url" in config and not os.path.exists(font_path):
|
||||
logger.info("Just a sec, downloading %s", name)
|
||||
download_file(config["url"], font_path)
|
|
@ -1,7 +1,11 @@
|
|||
""" functionality outline for a book data connector """
|
||||
from abc import ABC, abstractmethod
|
||||
import imghdr
|
||||
import ipaddress
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import transaction
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
@ -127,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:
|
||||
|
@ -248,6 +252,8 @@ def dict_from_mappings(data, mappings):
|
|||
def get_data(url, params=None, timeout=10):
|
||||
"""wrapper for request.get"""
|
||||
# check if the url is blocked
|
||||
raise_not_valid_url(url)
|
||||
|
||||
if models.FederatedServer.is_blocked(url):
|
||||
raise ConnectorException(f"Attempting to load data from blocked url: {url}")
|
||||
|
||||
|
@ -264,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:
|
||||
|
@ -272,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
|
||||
|
@ -280,6 +286,7 @@ def get_data(url, params=None, timeout=10):
|
|||
|
||||
def get_image(url, timeout=10):
|
||||
"""wrapper for requesting an image"""
|
||||
raise_not_valid_url(url)
|
||||
try:
|
||||
resp = requests.get(
|
||||
url,
|
||||
|
@ -289,11 +296,33 @@ def get_image(url, timeout=10):
|
|||
timeout=timeout,
|
||||
)
|
||||
except RequestException as err:
|
||||
logger.exception(err)
|
||||
return None
|
||||
logger.info(err)
|
||||
return None, None
|
||||
|
||||
if not resp.ok:
|
||||
return None
|
||||
return resp
|
||||
return None, None
|
||||
|
||||
image_content = ContentFile(resp.content)
|
||||
extension = imghdr.what(None, image_content.read())
|
||||
if not extension:
|
||||
logger.info("File requested was not an image: %s", url)
|
||||
return None, None
|
||||
|
||||
return image_content, extension
|
||||
|
||||
|
||||
def raise_not_valid_url(url):
|
||||
"""do some basic reality checks on the url"""
|
||||
parsed = urlparse(url)
|
||||
if not parsed.scheme in ["http", "https"]:
|
||||
raise ConnectorException("Invalid scheme: ", url)
|
||||
|
||||
try:
|
||||
ipaddress.ip_address(parsed.netloc)
|
||||
raise ConnectorException("Provided url is an IP address: ", url)
|
||||
except ValueError:
|
||||
# it's not an IP address, which is good
|
||||
pass
|
||||
|
||||
|
||||
class Mapping:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -68,7 +68,30 @@ class Connector(AbstractConnector):
|
|||
Mapping("born", remote_field="birth_date"),
|
||||
Mapping("died", remote_field="death_date"),
|
||||
Mapping("bio", formatter=get_description),
|
||||
Mapping("isni", remote_field="remote_ids", formatter=get_isni),
|
||||
Mapping(
|
||||
"isni",
|
||||
remote_field="remote_ids",
|
||||
formatter=lambda b: get_dict_field(b, "isni"),
|
||||
),
|
||||
Mapping(
|
||||
"asin",
|
||||
remote_field="remote_ids",
|
||||
formatter=lambda b: get_dict_field(b, "amazon"),
|
||||
),
|
||||
Mapping(
|
||||
"viaf",
|
||||
remote_field="remote_ids",
|
||||
formatter=lambda b: get_dict_field(b, "viaf"),
|
||||
),
|
||||
Mapping(
|
||||
"wikidata",
|
||||
remote_field="remote_ids",
|
||||
formatter=lambda b: get_dict_field(b, "wikidata"),
|
||||
),
|
||||
Mapping(
|
||||
"wikipedia_link", remote_field="links", formatter=get_wikipedia_link
|
||||
),
|
||||
Mapping("inventaire_id", remote_field="links", formatter=get_inventaire_id),
|
||||
]
|
||||
|
||||
def get_book_data(self, remote_id):
|
||||
|
@ -227,11 +250,38 @@ def get_languages(language_blob):
|
|||
return langs
|
||||
|
||||
|
||||
def get_isni(remote_ids_blob):
|
||||
def get_dict_field(blob, field_name):
|
||||
"""extract the isni from the remote id data for the author"""
|
||||
if not remote_ids_blob or not isinstance(remote_ids_blob, dict):
|
||||
if not blob or not isinstance(blob, dict):
|
||||
return None
|
||||
return blob.get(field_name)
|
||||
|
||||
|
||||
def get_wikipedia_link(links):
|
||||
"""extract wikipedia links"""
|
||||
if not isinstance(links, list):
|
||||
return None
|
||||
|
||||
for link in links:
|
||||
if not isinstance(link, dict):
|
||||
continue
|
||||
if link.get("title") == "wikipedia":
|
||||
return link.get("url")
|
||||
return None
|
||||
|
||||
|
||||
def get_inventaire_id(links):
|
||||
"""extract and format inventaire ids"""
|
||||
if not isinstance(links, list):
|
||||
return None
|
||||
|
||||
for link in links:
|
||||
if not isinstance(link, dict):
|
||||
continue
|
||||
if link.get("title") == "inventaire.io":
|
||||
iv_link = link.get("url")
|
||||
return iv_link.split("/")[-1]
|
||||
return None
|
||||
return remote_ids_blob.get("isni")
|
||||
|
||||
|
||||
def pick_default_edition(options):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -48,7 +48,9 @@ def moderation_report_email(report):
|
|||
data["reportee"] = report.user.localname or report.user.username
|
||||
data["report_link"] = report.remote_id
|
||||
|
||||
for admin in models.User.objects.filter(groups__name__in=["admin", "moderator"]):
|
||||
for admin in models.User.objects.filter(
|
||||
groups__name__in=["admin", "moderator"]
|
||||
).distinct():
|
||||
data["user"] = admin.display_name
|
||||
send_email.delay(admin.email, *format_email("moderation_report", data))
|
||||
|
||||
|
|
|
@ -1,516 +0,0 @@
|
|||
""" using django model forms """
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
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()}
|
||||
|
||||
|
||||
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"]
|
||||
|
||||
|
||||
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"}
|
||||
),
|
||||
"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 = []
|
||||
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", "statuses", "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"]
|
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"}
|
||||
),
|
||||
}
|
104
bookwyrm/forms/books.py
Normal file
104
bookwyrm/forms/books.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
""" using django model forms """
|
||||
from django import forms
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models.fields import ClearableFileInputWithWarning
|
||||
from .custom_form import CustomForm
|
||||
from .widgets import ArrayWidget, SelectDateWidget, Select
|
||||
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
class CoverForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Book
|
||||
fields = ["cover"]
|
||||
help_texts = {f: None for f in fields}
|
||||
|
||||
|
||||
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": SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_first_published_date"}
|
||||
),
|
||||
"published_date": SelectDateWidget(
|
||||
attrs={"aria-describedby": "desc_published_date"}
|
||||
),
|
||||
"cover": ClearableFileInputWithWarning(
|
||||
attrs={"aria-describedby": "desc_cover"}
|
||||
),
|
||||
"physical_format": 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):
|
||||
"""don't let readthroughs end before they start"""
|
||||
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", "answer"]
|
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"]
|
70
bookwyrm/forms/widgets.py
Normal file
70
bookwyrm/forms/widgets.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
""" using django model forms """
|
||||
from django import forms
|
||||
|
||||
|
||||
class ArrayWidget(forms.widgets.TextInput):
|
||||
"""Inputs for postgres array fields"""
|
||||
|
||||
# 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 Select(forms.Select):
|
||||
"""custom template for select widget"""
|
||||
|
||||
template_name = "widgets/select.html"
|
||||
|
||||
|
||||
class SelectDateWidget(forms.SelectDateWidget):
|
||||
"""
|
||||
A widget that splits date input into two <select> boxes and a numerical year.
|
||||
"""
|
||||
|
||||
template_name = "widgets/addon_multiwidget.html"
|
||||
select_widget = Select
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
"""sets individual widgets"""
|
||||
context = super().get_context(name, value, attrs)
|
||||
date_context = {}
|
||||
year_name = self.year_field % name
|
||||
date_context["year"] = forms.NumberInput().get_context(
|
||||
name=year_name,
|
||||
value=context["widget"]["value"]["year"],
|
||||
attrs={
|
||||
**context["widget"]["attrs"],
|
||||
"id": f"id_{year_name}",
|
||||
"class": "input",
|
||||
},
|
||||
)
|
||||
month_choices = list(self.months.items())
|
||||
if not self.is_required:
|
||||
month_choices.insert(0, self.month_none_value)
|
||||
month_name = self.month_field % name
|
||||
date_context["month"] = self.select_widget(
|
||||
attrs, choices=month_choices
|
||||
).get_context(
|
||||
name=month_name,
|
||||
value=context["widget"]["value"]["month"],
|
||||
attrs={**context["widget"]["attrs"], "id": f"id_{month_name}"},
|
||||
)
|
||||
day_choices = [(i, i) for i in range(1, 32)]
|
||||
if not self.is_required:
|
||||
day_choices.insert(0, self.day_none_value)
|
||||
day_name = self.day_field % name
|
||||
date_context["day"] = self.select_widget(
|
||||
attrs,
|
||||
choices=day_choices,
|
||||
).get_context(
|
||||
name=day_name,
|
||||
value=context["widget"]["value"]["day"],
|
||||
attrs={**context["widget"]["attrs"], "id": f"id_{day_name}"},
|
||||
)
|
||||
subwidgets = []
|
||||
for field in self._parse_date_fmt():
|
||||
subwidgets.append(date_context[field]["widget"])
|
||||
context["widget"]["subwidgets"] = subwidgets
|
||||
return context
|
23
bookwyrm/management/commands/admin_code.py
Normal file
23
bookwyrm/management/commands/admin_code.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
""" Get your admin code to allow install """
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from bookwyrm import models
|
||||
|
||||
|
||||
def get_admin_code():
|
||||
"""get that code"""
|
||||
return models.SiteSettings.objects.get().admin_code
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""command-line options"""
|
||||
|
||||
help = "Gets admin code for configuring BookWyrm"
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def handle(self, *args, **options):
|
||||
"""execute init"""
|
||||
self.stdout.write("*******************************************")
|
||||
self.stdout.write("Use this code to create your admin account:")
|
||||
self.stdout.write(get_admin_code())
|
||||
self.stdout.write("*******************************************")
|
|
@ -7,6 +7,7 @@ from bookwyrm import settings
|
|||
r = redis.Redis(
|
||||
host=settings.REDIS_ACTIVITY_HOST,
|
||||
port=settings.REDIS_ACTIVITY_PORT,
|
||||
password=settings.REDIS_ACTIVITY_PASSWORD,
|
||||
db=settings.REDIS_ACTIVITY_DB_INDEX,
|
||||
)
|
||||
|
||||
|
|
|
@ -10,7 +10,9 @@ class Command(BaseCommand):
|
|||
|
||||
help = "Generate preview images"
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def add_arguments(self, parser):
|
||||
"""options for how the command is run"""
|
||||
parser.add_argument(
|
||||
"--all",
|
||||
"-a",
|
||||
|
@ -38,6 +40,7 @@ class Command(BaseCommand):
|
|||
preview_images.generate_site_preview_image_task.delay()
|
||||
self.stdout.write(" OK 🖼")
|
||||
|
||||
# pylint: disable=consider-using-f-string
|
||||
if options["all"]:
|
||||
# Users
|
||||
users = models.User.objects.filter(
|
||||
|
|
|
@ -120,6 +120,7 @@ def init_settings():
|
|||
models.SiteSettings.objects.create(
|
||||
support_link="https://www.patreon.com/bookwyrm",
|
||||
support_title="Patreon",
|
||||
install_mode=True,
|
||||
)
|
||||
|
||||
|
||||
|
|
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)
|
37
bookwyrm/migrations/0132_alter_user_preferred_language.py
Normal file
37
bookwyrm/migrations/0132_alter_user_preferred_language.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Generated by Django 3.2.10 on 2022-02-02 20:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0131_merge_20220125_1644"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="preferred_language",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("en-us", "English"),
|
||||
("de-de", "Deutsch (German)"),
|
||||
("es-es", "Español (Spanish)"),
|
||||
("gl-es", "Galego (Galician)"),
|
||||
("it-it", "Italiano (Italian)"),
|
||||
("fr-fr", "Français (French)"),
|
||||
("lt-lt", "Lietuvių (Lithuanian)"),
|
||||
("no-no", "Norsk (Norwegian)"),
|
||||
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
||||
("pt-pt", "Português Europeu (European Portuguese)"),
|
||||
("sv-se", "Svenska (Swedish)"),
|
||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||
],
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
21
bookwyrm/migrations/0133_alter_listitem_notes.py
Normal file
21
bookwyrm/migrations/0133_alter_listitem_notes.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 3.2.11 on 2022-02-04 20:06
|
||||
|
||||
import bookwyrm.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0132_alter_user_preferred_language"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="listitem",
|
||||
name="notes",
|
||||
field=bookwyrm.models.fields.HtmlField(
|
||||
blank=True, max_length=300, null=True
|
||||
),
|
||||
),
|
||||
]
|
29
bookwyrm/migrations/0134_announcement_display_type.py
Normal file
29
bookwyrm/migrations/0134_announcement_display_type.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 3.2.11 on 2022-02-11 18:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0133_alter_listitem_notes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="announcement",
|
||||
name="display_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("white-ter", "None"),
|
||||
("primary-light", "Primary"),
|
||||
("success-light", "Success"),
|
||||
("link-light", "Link"),
|
||||
("warning-light", "Warning"),
|
||||
("danger-light", "Danger"),
|
||||
],
|
||||
default="white-ter",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
58
bookwyrm/migrations/0135_auto_20220217_1624.py
Normal file
58
bookwyrm/migrations/0135_auto_20220217_1624.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
# Generated by Django 3.2.12 on 2022-02-17 16:24
|
||||
|
||||
import bookwyrm.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0134_announcement_display_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="author",
|
||||
old_name="viaf_id",
|
||||
new_name="viaf",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="edition",
|
||||
name="asin",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="author",
|
||||
name="asin",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="author",
|
||||
name="wikidata",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="book",
|
||||
name="asin",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="book",
|
||||
name="viaf",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="book",
|
||||
name="wikidata",
|
||||
field=bookwyrm.models.fields.CharField(
|
||||
blank=True, max_length=255, null=True
|
||||
),
|
||||
),
|
||||
]
|
24
bookwyrm/migrations/0136_auto_20220217_1708.py
Normal file
24
bookwyrm/migrations/0136_auto_20220217_1708.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.2.12 on 2022-02-17 17:08
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0135_auto_20220217_1624"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="sitesettings",
|
||||
name="admin_code",
|
||||
field=models.CharField(default=uuid.uuid4, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="sitesettings",
|
||||
name="install_mode",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.12 on 2022-02-17 19:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0136_auto_20220217_1708"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="sitesettings",
|
||||
name="allow_registration",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
39
bookwyrm/migrations/0138_automod.py
Normal file
39
bookwyrm/migrations/0138_automod.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
# Generated by Django 3.2.12 on 2022-02-24 18:59
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0137_alter_sitesettings_allow_registration"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="AutoMod",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("string_match", models.CharField(max_length=200, unique=True)),
|
||||
("flag_users", models.BooleanField(default=True)),
|
||||
("flag_statuses", models.BooleanField(default=True)),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
45
bookwyrm/migrations/0139_report_status.py
Normal file
45
bookwyrm/migrations/0139_report_status.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Generated by Django 3.2.12 on 2022-02-24 20:41
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def set_report_statuses(apps, schema_editor):
|
||||
"""copy over status fields"""
|
||||
db_alias = schema_editor.connection.alias
|
||||
report_model = apps.get_model("bookwyrm", "Report")
|
||||
reports = report_model.objects.using(db_alias).filter(statuses__isnull=False)
|
||||
for report in reports:
|
||||
report.status = report.statuses.first()
|
||||
report.save()
|
||||
|
||||
|
||||
def set_reverse(apps, schema_editor):
|
||||
"""copy over status fields"""
|
||||
db_alias = schema_editor.connection.alias
|
||||
report_model = apps.get_model("bookwyrm", "Report")
|
||||
reports = report_model.objects.using(db_alias).filter(status__isnull=False)
|
||||
for report in reports:
|
||||
report.statuses.set(report.status)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0138_automod"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="report",
|
||||
name="status",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="reports",
|
||||
to="bookwyrm.status",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(set_report_statuses, reverse_code=set_reverse),
|
||||
]
|
17
bookwyrm/migrations/0140_remove_report_statuses.py
Normal file
17
bookwyrm/migrations/0140_remove_report_statuses.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.2.12 on 2022-02-24 20:43
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0139_report_status"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="report",
|
||||
name="statuses",
|
||||
),
|
||||
]
|
24
bookwyrm/migrations/0141_alter_report_status.py
Normal file
24
bookwyrm/migrations/0141_alter_report_status.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.2.12 on 2022-02-24 20:50
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0140_remove_report_statuses"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="report",
|
||||
name="status",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="bookwyrm.status",
|
||||
),
|
||||
),
|
||||
]
|
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),
|
||||
),
|
||||
]
|
30
bookwyrm/migrations/0146_auto_20220316_2352.py
Normal file
30
bookwyrm/migrations/0146_auto_20220316_2352.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Generated by Django 3.2.12 on 2022-03-16 23:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0145_sitesettings_version"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="inviterequest",
|
||||
name="answer",
|
||||
field=models.TextField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="sitesettings",
|
||||
name="invite_question_text",
|
||||
field=models.CharField(
|
||||
blank=True, default="What is your favourite book?", max_length=255
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="sitesettings",
|
||||
name="invite_request_question",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
38
bookwyrm/migrations/0147_alter_user_preferred_language.py
Normal file
38
bookwyrm/migrations/0147_alter_user_preferred_language.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Generated by Django 3.2.12 on 2022-03-26 16:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0146_auto_20220316_2352"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="preferred_language",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("en-us", "English"),
|
||||
("de-de", "Deutsch (German)"),
|
||||
("es-es", "Español (Spanish)"),
|
||||
("gl-es", "Galego (Galician)"),
|
||||
("it-it", "Italiano (Italian)"),
|
||||
("fr-fr", "Français (French)"),
|
||||
("lt-lt", "Lietuvių (Lithuanian)"),
|
||||
("no-no", "Norsk (Norwegian)"),
|
||||
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
||||
("pt-pt", "Português Europeu (European Portuguese)"),
|
||||
("ro-ro", "Română (Romanian)"),
|
||||
("sv-se", "Svenska (Swedish)"),
|
||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
||||
],
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -26,10 +26,10 @@ 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
|
||||
from .antispam import EmailBlocklist, IPBlocklist, AutoMod, automod_task
|
||||
|
||||
from .notification import Notification
|
||||
|
||||
|
|
|
@ -2,10 +2,20 @@
|
|||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .base_model import BookWyrmModel
|
||||
|
||||
|
||||
DisplayTypes = [
|
||||
("primary-light", _("Primary")),
|
||||
("success-light", _("Success")),
|
||||
("link-light", _("Link")),
|
||||
("warning-light", _("Warning")),
|
||||
("danger-light", _("Danger")),
|
||||
]
|
||||
|
||||
|
||||
class Announcement(BookWyrmModel):
|
||||
"""The admin has something to say"""
|
||||
|
||||
|
@ -16,6 +26,9 @@ class Announcement(BookWyrmModel):
|
|||
start_date = models.DateTimeField(blank=True, null=True)
|
||||
end_date = models.DateTimeField(blank=True, null=True)
|
||||
active = models.BooleanField(default=True)
|
||||
display_type = models.CharField(
|
||||
max_length=20, choices=DisplayTypes, null=True, blank=True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def active_announcements(cls):
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
""" Lets try NOT to sell viagra """
|
||||
from django.db import models
|
||||
from functools import reduce
|
||||
import operator
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from bookwyrm.tasks import app
|
||||
from .user import User
|
||||
|
||||
|
||||
|
@ -33,3 +40,107 @@ class IPBlocklist(models.Model):
|
|||
"""default sorting"""
|
||||
|
||||
ordering = ("-created_date",)
|
||||
|
||||
|
||||
class AutoMod(models.Model):
|
||||
"""rules to automatically flag suspicious activity"""
|
||||
|
||||
string_match = models.CharField(max_length=200, unique=True)
|
||||
flag_users = models.BooleanField(default=True)
|
||||
flag_statuses = models.BooleanField(default=True)
|
||||
created_by = models.ForeignKey("User", on_delete=models.PROTECT)
|
||||
|
||||
|
||||
@app.task(queue="low_priority")
|
||||
def automod_task():
|
||||
"""Create reports"""
|
||||
if not AutoMod.objects.exists():
|
||||
return
|
||||
reporter = AutoMod.objects.first().created_by
|
||||
reports = automod_users(reporter) + automod_statuses(reporter)
|
||||
if reports:
|
||||
admins = User.objects.filter(
|
||||
models.Q(user_permissions__name__in=["moderate_user", "moderate_post"])
|
||||
| models.Q(is_superuser=True)
|
||||
).all()
|
||||
notification_model = apps.get_model(
|
||||
"bookwyrm", "Notification", require_ready=True
|
||||
)
|
||||
for admin in admins:
|
||||
notification_model.objects.bulk_create(
|
||||
[
|
||||
notification_model(
|
||||
user=admin,
|
||||
related_report=r,
|
||||
notification_type="REPORT",
|
||||
)
|
||||
for r in reports
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def automod_users(reporter):
|
||||
"""check users for moderation flags"""
|
||||
user_rules = AutoMod.objects.filter(flag_users=True).values_list(
|
||||
"string_match", flat=True
|
||||
)
|
||||
if not user_rules:
|
||||
return []
|
||||
|
||||
filters = []
|
||||
for field in ["username", "summary", "name"]:
|
||||
filters += [{f"{field}__icontains": r} for r in user_rules]
|
||||
users = User.objects.filter(
|
||||
reduce(operator.or_, (Q(**f) for f in filters)),
|
||||
is_active=True,
|
||||
local=True,
|
||||
report__isnull=True, # don't flag users that already have reports
|
||||
).distinct()
|
||||
|
||||
report_model = apps.get_model("bookwyrm", "Report", require_ready=True)
|
||||
|
||||
return report_model.objects.bulk_create(
|
||||
[
|
||||
report_model(
|
||||
reporter=reporter,
|
||||
note=_("Automatically generated report"),
|
||||
user=u,
|
||||
)
|
||||
for u in users
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def automod_statuses(reporter):
|
||||
"""check statues for moderation flags"""
|
||||
status_rules = AutoMod.objects.filter(flag_statuses=True).values_list(
|
||||
"string_match", flat=True
|
||||
)
|
||||
|
||||
if not status_rules:
|
||||
return []
|
||||
|
||||
filters = []
|
||||
for field in ["content", "content_warning", "quotation__quote", "review__name"]:
|
||||
filters += [{f"{field}__icontains": r} for r in status_rules]
|
||||
|
||||
status_model = apps.get_model("bookwyrm", "Status", require_ready=True)
|
||||
statuses = status_model.objects.filter(
|
||||
reduce(operator.or_, (Q(**f) for f in filters)),
|
||||
deleted=False,
|
||||
local=True,
|
||||
report__isnull=True, # don't flag statuses that already have reports
|
||||
).distinct()
|
||||
|
||||
report_model = apps.get_model("bookwyrm", "Report", require_ready=True)
|
||||
return report_model.objects.bulk_create(
|
||||
[
|
||||
report_model(
|
||||
reporter=reporter,
|
||||
note=_("Automatically generated report"),
|
||||
user=s.user,
|
||||
status=s,
|
||||
)
|
||||
for s in statuses
|
||||
]
|
||||
)
|
||||
|
|
|
@ -21,9 +21,6 @@ class Author(BookDataModel):
|
|||
isni = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
viaf_id = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
gutenberg_id = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
|
|
|
@ -46,6 +46,15 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
|
|||
bnf_id = fields.CharField( # Bibliothèque nationale de France
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
viaf = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
wikidata = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
asin = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
search_vector = SearchVectorField(null=True)
|
||||
|
||||
last_edited_by = fields.ForeignKey(
|
||||
|
@ -271,9 +280,6 @@ class Edition(Book):
|
|||
oclc_number = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
asin = fields.CharField(
|
||||
max_length=255, blank=True, null=True, deduplication_field=True
|
||||
)
|
||||
pages = fields.IntegerField(blank=True, null=True)
|
||||
physical_format = fields.CharField(
|
||||
max_length=255, choices=FormatChoices, null=True, blank=True
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
""" activitypub-aware django model fields """
|
||||
from dataclasses import MISSING
|
||||
import imghdr
|
||||
import re
|
||||
from uuid import uuid4
|
||||
from urllib.parse import urljoin
|
||||
|
@ -9,7 +8,6 @@ import dateutil.parser
|
|||
from dateutil.parser import ParserError
|
||||
from django.contrib.postgres.fields import ArrayField as DjangoArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import models
|
||||
from django.forms import ClearableFileInput, ImageField as DjangoImageField
|
||||
from django.utils import timezone
|
||||
|
@ -391,7 +389,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
|
|||
self.alt_field = alt_field
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
# pylint: disable=arguments-differ,arguments-renamed
|
||||
def set_field_from_activity(self, instance, data, save=True, overwrite=True):
|
||||
"""helper function for assinging a value to the field"""
|
||||
value = getattr(data, self.get_activitypub_field())
|
||||
|
@ -443,12 +441,10 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
|
|||
except ValidationError:
|
||||
return None
|
||||
|
||||
response = get_image(url)
|
||||
if not response:
|
||||
image_content, extension = get_image(url)
|
||||
if not image_content:
|
||||
return None
|
||||
|
||||
image_content = ContentFile(response.content)
|
||||
extension = imghdr.what(None, image_content.read()) or ""
|
||||
image_name = f"{uuid4()}.{extension}"
|
||||
return [image_name, image_content]
|
||||
|
||||
|
|
|
@ -142,7 +142,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
|
|||
user = fields.ForeignKey(
|
||||
"User", on_delete=models.PROTECT, activitypub_field="actor"
|
||||
)
|
||||
notes = fields.TextField(blank=True, null=True, max_length=300)
|
||||
notes = fields.HtmlField(blank=True, null=True, max_length=300)
|
||||
approved = models.BooleanField(default=True)
|
||||
order = fields.IntegerField()
|
||||
endorsement = models.ManyToManyField("User", related_name="endorsers")
|
||||
|
|
|
@ -39,15 +39,14 @@ class UserRelationship(BookWyrmModel):
|
|||
|
||||
def save(self, *args, **kwargs):
|
||||
"""clear the template cache"""
|
||||
# invalidate the template cache
|
||||
cache.delete_many(
|
||||
[
|
||||
f"relationship-{self.user_subject.id}-{self.user_object.id}",
|
||||
f"relationship-{self.user_object.id}-{self.user_subject.id}",
|
||||
]
|
||||
)
|
||||
clear_cache(self.user_subject, self.user_object)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""clear the template cache"""
|
||||
clear_cache(self.user_subject, self.user_object)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
"""relationships should be unique"""
|
||||
|
||||
|
@ -90,7 +89,9 @@ class UserFollows(ActivityMixin, UserRelationship):
|
|||
user_object=self.user_subject,
|
||||
)
|
||||
).exists():
|
||||
raise IntegrityError()
|
||||
raise IntegrityError(
|
||||
"Attempting to follow blocked user", self.user_subject, self.user_object
|
||||
)
|
||||
# don't broadcast this type of relationship -- accepts and requests
|
||||
# are handled by the UserFollowRequest model
|
||||
super().save(*args, broadcast=False, **kwargs)
|
||||
|
@ -98,11 +99,12 @@ class UserFollows(ActivityMixin, UserRelationship):
|
|||
@classmethod
|
||||
def from_request(cls, follow_request):
|
||||
"""converts a follow request into a follow relationship"""
|
||||
return cls.objects.create(
|
||||
obj, _ = cls.objects.get_or_create(
|
||||
user_subject=follow_request.user_subject,
|
||||
user_object=follow_request.user_object,
|
||||
remote_id=follow_request.remote_id,
|
||||
)
|
||||
return obj
|
||||
|
||||
|
||||
class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||
|
@ -133,7 +135,9 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
|||
user_object=self.user_subject,
|
||||
)
|
||||
).exists():
|
||||
raise IntegrityError()
|
||||
raise IntegrityError(
|
||||
"Attempting to follow blocked user", self.user_subject, self.user_object
|
||||
)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if broadcast and self.user_subject.local and not self.user_object.local:
|
||||
|
@ -174,6 +178,7 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
|||
|
||||
with transaction.atomic():
|
||||
UserFollows.from_request(self)
|
||||
if self.id:
|
||||
self.delete()
|
||||
|
||||
def reject(self):
|
||||
|
@ -207,3 +212,13 @@ class UserBlocks(ActivityMixin, UserRelationship):
|
|||
Q(user_subject=self.user_subject, user_object=self.user_object)
|
||||
| Q(user_subject=self.user_object, user_object=self.user_subject)
|
||||
).delete()
|
||||
|
||||
|
||||
def clear_cache(user_subject, user_object):
|
||||
"""clear relationship cache"""
|
||||
cache.delete_many(
|
||||
[
|
||||
f"relationship-{user_subject.id}-{user_object.id}",
|
||||
f"relationship-{user_object.id}-{user_subject.id}",
|
||||
]
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
""" flagged for moderation """
|
||||
from django.db import models
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from .base_model import BookWyrmModel
|
||||
|
||||
|
||||
|
@ -11,10 +12,18 @@ class Report(BookWyrmModel):
|
|||
)
|
||||
note = models.TextField(null=True, blank=True)
|
||||
user = models.ForeignKey("User", on_delete=models.PROTECT)
|
||||
statuses = models.ManyToManyField("Status", blank=True)
|
||||
status = models.ForeignKey(
|
||||
"Status",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
links = models.ManyToManyField("Link", blank=True)
|
||||
resolved = models.BooleanField(default=False)
|
||||
|
||||
def get_remote_id(self):
|
||||
return f"https://{DOMAIN}/settings/reports/{self.id}"
|
||||
|
||||
class Meta:
|
||||
"""set order by default"""
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
""" the particulars for this instance of BookWyrm """
|
||||
import datetime
|
||||
from urllib.parse import urljoin
|
||||
import uuid
|
||||
|
||||
from django.db import models, IntegrityError
|
||||
from django.dispatch import receiver
|
||||
|
@ -23,6 +24,14 @@ 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)
|
||||
admin_code = models.CharField(max_length=50, default=uuid.uuid4)
|
||||
|
||||
# about page
|
||||
registration_closed_text = models.TextField(
|
||||
|
@ -38,10 +47,14 @@ class SiteSettings(models.Model):
|
|||
privacy_policy = models.TextField(default="Add a privacy policy here.")
|
||||
|
||||
# registration
|
||||
allow_registration = models.BooleanField(default=True)
|
||||
allow_registration = models.BooleanField(default=False)
|
||||
allow_invite_requests = models.BooleanField(default=True)
|
||||
invite_request_question = models.BooleanField(default=False)
|
||||
require_confirm_email = models.BooleanField(default=True)
|
||||
|
||||
invite_question_text = models.CharField(
|
||||
max_length=255, blank=True, default="What is your favourite book?"
|
||||
)
|
||||
# images
|
||||
logo = models.ImageField(upload_to="logos/", null=True, blank=True)
|
||||
logo_small = models.ImageField(upload_to="logos/", null=True, blank=True)
|
||||
|
@ -91,14 +104,29 @@ class SiteSettings(models.Model):
|
|||
return urljoin(STATIC_FULL_URL, default_path)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""if require_confirm_email is disabled, make sure no users are pending"""
|
||||
"""if require_confirm_email is disabled, make sure no users are pending,
|
||||
if enabled, make sure invite_question_text is not empty"""
|
||||
if not self.require_confirm_email:
|
||||
User.objects.filter(is_active=False, deactivation_reason="pending").update(
|
||||
is_active=True, deactivation_reason=None
|
||||
)
|
||||
if not self.invite_question_text:
|
||||
self.invite_question_text = "What is your favourite book?"
|
||||
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"""
|
||||
|
||||
|
@ -129,6 +157,7 @@ class InviteRequest(BookWyrmModel):
|
|||
invite = models.ForeignKey(
|
||||
SiteInvite, on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
answer = models.TextField(max_length=50, unique=False, null=True, blank=True)
|
||||
invite_sent = models.BooleanField(default=False)
|
||||
ignored = models.BooleanField(default=False)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import os
|
|||
import textwrap
|
||||
from io import BytesIO
|
||||
from uuid import uuid4
|
||||
import logging
|
||||
|
||||
import colorsys
|
||||
from colorthief import ColorThief
|
||||
|
@ -17,34 +18,49 @@ from django.db.models import Avg
|
|||
from bookwyrm import models, settings
|
||||
from bookwyrm.tasks import app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
IMG_WIDTH = settings.PREVIEW_IMG_WIDTH
|
||||
IMG_HEIGHT = settings.PREVIEW_IMG_HEIGHT
|
||||
BG_COLOR = settings.PREVIEW_BG_COLOR
|
||||
TEXT_COLOR = settings.PREVIEW_TEXT_COLOR
|
||||
DEFAULT_COVER_COLOR = settings.PREVIEW_DEFAULT_COVER_COLOR
|
||||
DEFAULT_FONT = settings.PREVIEW_DEFAULT_FONT
|
||||
TRANSPARENT_COLOR = (0, 0, 0, 0)
|
||||
|
||||
margin = math.floor(IMG_HEIGHT / 10)
|
||||
gutter = math.floor(margin / 2)
|
||||
inner_img_height = math.floor(IMG_HEIGHT * 0.8)
|
||||
inner_img_width = math.floor(inner_img_height * 0.7)
|
||||
font_dir = os.path.join(settings.STATIC_ROOT, "fonts/public_sans")
|
||||
|
||||
|
||||
def get_font(font_name, size=28):
|
||||
"""Loads custom font"""
|
||||
if font_name == "light":
|
||||
font_path = os.path.join(font_dir, "PublicSans-Light.ttf")
|
||||
if font_name == "regular":
|
||||
font_path = os.path.join(font_dir, "PublicSans-Regular.ttf")
|
||||
elif font_name == "bold":
|
||||
font_path = os.path.join(font_dir, "PublicSans-Bold.ttf")
|
||||
def get_imagefont(name, size):
|
||||
"""Loads an ImageFont based on config"""
|
||||
try:
|
||||
config = settings.FONTS[name]
|
||||
path = os.path.join(settings.FONT_DIR, config["directory"], config["filename"])
|
||||
return ImageFont.truetype(path, size)
|
||||
except KeyError:
|
||||
logger.error("Font %s not found in config", name)
|
||||
except OSError:
|
||||
logger.error("Could not load font %s from file", name)
|
||||
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def get_font(weight, size=28):
|
||||
"""Gets a custom font with the given weight and size"""
|
||||
font = get_imagefont(DEFAULT_FONT, size)
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, size)
|
||||
except OSError:
|
||||
font = ImageFont.load_default()
|
||||
if weight == "light":
|
||||
font.set_variation_by_name("Light")
|
||||
if weight == "bold":
|
||||
font.set_variation_by_name("Bold")
|
||||
if weight == "regular":
|
||||
font.set_variation_by_name("Regular")
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return font
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
|
|||
"ol",
|
||||
"li",
|
||||
]
|
||||
self.allowed_attrs = ["href", "rel", "src", "alt"]
|
||||
self.tag_stack = []
|
||||
self.output = []
|
||||
# if the html appears invalid, we just won't allow any at all
|
||||
|
@ -30,7 +31,14 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
|
|||
def handle_starttag(self, tag, attrs):
|
||||
"""check if the tag is valid"""
|
||||
if self.allow_html and tag in self.allowed_tags:
|
||||
self.output.append(("tag", self.get_starttag_text()))
|
||||
allowed_attrs = " ".join(
|
||||
f'{a}="{v}"' for a, v in attrs if a in self.allowed_attrs
|
||||
)
|
||||
reconstructed = f"<{tag}"
|
||||
if allowed_attrs:
|
||||
reconstructed += " " + allowed_attrs
|
||||
reconstructed += ">"
|
||||
self.output.append(("tag", reconstructed))
|
||||
self.tag_stack.append(tag)
|
||||
else:
|
||||
self.output.append(("data", ""))
|
||||
|
|
|
@ -6,15 +6,22 @@ import requests
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
env = Env()
|
||||
env.read_env()
|
||||
DOMAIN = env("DOMAIN")
|
||||
VERSION = "0.2.0"
|
||||
VERSION = "0.3.4"
|
||||
|
||||
RELEASE_API = env(
|
||||
"RELEASE_API",
|
||||
"https://api.github.com/repos/bookwyrm-social/bookwyrm/releases/latest",
|
||||
)
|
||||
|
||||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||
|
||||
JS_CACHE = "7b5303af"
|
||||
JS_CACHE = "bc93172a"
|
||||
|
||||
# email
|
||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||
|
@ -35,6 +42,9 @@ LOCALE_PATHS = [
|
|||
]
|
||||
LANGUAGE_COOKIE_NAME = env.str("LANGUAGE_COOKIE_NAME", "django_language")
|
||||
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||
|
||||
# Preview image
|
||||
|
@ -44,6 +54,16 @@ PREVIEW_TEXT_COLOR = env.str("PREVIEW_TEXT_COLOR", "#363636")
|
|||
PREVIEW_IMG_WIDTH = env.int("PREVIEW_IMG_WIDTH", 1200)
|
||||
PREVIEW_IMG_HEIGHT = env.int("PREVIEW_IMG_HEIGHT", 630)
|
||||
PREVIEW_DEFAULT_COVER_COLOR = env.str("PREVIEW_DEFAULT_COVER_COLOR", "#002549")
|
||||
PREVIEW_DEFAULT_FONT = env.str("PREVIEW_DEFAULT_FONT", "Source Han Sans")
|
||||
|
||||
FONTS = {
|
||||
"Source Han Sans": {
|
||||
"directory": "source_han_sans",
|
||||
"filename": "SourceHanSans-VF.ttf.ttc",
|
||||
"url": "https://github.com/adobe-fonts/source-han-sans/raw/release/Variable/OTC/SourceHanSans-VF.ttf.ttc",
|
||||
}
|
||||
}
|
||||
FONT_DIR = os.path.join(STATIC_ROOT, "fonts")
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||
|
@ -53,7 +73,7 @@ SECRET_KEY = env("SECRET_KEY")
|
|||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = env.bool("DEBUG", True)
|
||||
USE_HTTPS = env.bool("USE_HTTPS", False)
|
||||
USE_HTTPS = env.bool("USE_HTTPS", not DEBUG)
|
||||
|
||||
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", ["*"])
|
||||
|
||||
|
@ -67,9 +87,10 @@ INSTALLED_APPS = [
|
|||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.humanize",
|
||||
"django_rename_app",
|
||||
"sass_processor",
|
||||
"bookwyrm",
|
||||
"celery",
|
||||
"django_celery_beat",
|
||||
"imagekit",
|
||||
"storages",
|
||||
"debug_toolbar",
|
||||
|
@ -152,6 +173,9 @@ LOGGING = {
|
|||
"handlers": ["console", "mail_admins"],
|
||||
"level": LOG_LEVEL,
|
||||
},
|
||||
"django.utils.autoreload": {
|
||||
"level": "INFO",
|
||||
},
|
||||
# Add a bookwyrm-specific logger
|
||||
"bookwyrm": {
|
||||
"handlers": ["console"],
|
||||
|
@ -160,6 +184,18 @@ LOGGING = {
|
|||
},
|
||||
}
|
||||
|
||||
STATICFILES_FINDERS = [
|
||||
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||
"sass_processor.finders.CssFinder",
|
||||
]
|
||||
|
||||
SASS_PROCESSOR_INCLUDE_FILE_PATTERN = r"^.+\.[s]{0,1}(?:a|c)ss$"
|
||||
SASS_PROCESSOR_ENABLED = True
|
||||
|
||||
# minify css is production but not dev
|
||||
if not DEBUG:
|
||||
SASS_OUTPUT_STYLE = "compressed"
|
||||
|
||||
WSGI_APPLICATION = "bookwyrm.wsgi.application"
|
||||
|
||||
|
@ -190,7 +226,6 @@ if env("USE_DUMMY_CACHE", False):
|
|||
}
|
||||
}
|
||||
else:
|
||||
# pylint: disable=line-too-long
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
|
@ -225,7 +260,6 @@ AUTH_USER_MODEL = "bookwyrm.User"
|
|||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
|
@ -257,7 +291,8 @@ LANGUAGES = [
|
|||
("no-no", _("Norsk (Norwegian)")),
|
||||
("pt-br", _("Português do Brasil (Brazilian Portuguese)")),
|
||||
("pt-pt", _("Português Europeu (European Portuguese)")),
|
||||
("sv-se", _("Swedish (Svenska)")),
|
||||
("ro-ro", _("Română (Romanian)")),
|
||||
("sv-se", _("Svenska (Swedish)")),
|
||||
("zh-hans", _("简体中文 (Simplified Chinese)")),
|
||||
("zh-hant", _("繁體中文 (Traditional Chinese)")),
|
||||
]
|
||||
|
@ -313,17 +348,15 @@ if USE_S3:
|
|||
MEDIA_FULL_URL = MEDIA_URL
|
||||
STATIC_FULL_URL = STATIC_URL
|
||||
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
|
||||
# I don't know if it's used, but the site crashes without it
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
|
||||
else:
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))
|
||||
MEDIA_URL = "/images/"
|
||||
MEDIA_FULL_URL = f"{PROTOCOL}://{DOMAIN}{MEDIA_URL}"
|
||||
STATIC_FULL_URL = f"{PROTOCOL}://{DOMAIN}{STATIC_URL}"
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images"))
|
||||
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT = env("OTEL_EXPORTER_OTLP_ENDPOINT", None)
|
||||
OTEL_EXPORTER_OTLP_HEADERS = env("OTEL_EXPORTER_OTLP_HEADERS", None)
|
||||
OTEL_SERVICE_NAME = env("OTEL_SERVICE_NAME", None)
|
||||
|
||||
def show_toolbar(_):
|
||||
"""workaround for docker"""
|
||||
|
|
File diff suppressed because it is too large
Load diff
4
bookwyrm/static/css/bookwyrm.scss
Normal file
4
bookwyrm/static/css/bookwyrm.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
@charset "utf-8";
|
||||
|
||||
@import "vendor/bulma/bulma.sass";
|
||||
@import "bookwyrm/all.scss";
|
164
bookwyrm/static/css/bookwyrm/_all.scss
Normal file
164
bookwyrm/static/css/bookwyrm/_all.scss
Normal file
|
@ -0,0 +1,164 @@
|
|||
/** Imports
|
||||
******************************************************************************/
|
||||
@import "components/avatar";
|
||||
@import "components/barcode";
|
||||
@import "components/book_cover";
|
||||
@import "components/book_grid";
|
||||
@import "components/book_list";
|
||||
@import "components/book_preview_table";
|
||||
@import "components/breadcrumbs";
|
||||
@import "components/copy";
|
||||
@import "components/details";
|
||||
@import "components/file_input";
|
||||
@import "components/live_message";
|
||||
@import "components/shelving";
|
||||
@import "components/stars";
|
||||
@import "components/status";
|
||||
@import "components/tabs";
|
||||
@import "components/toggle";
|
||||
|
||||
@import "overrides/bulma_overrides";
|
||||
|
||||
@import "utilities/a11y";
|
||||
@import "utilities/alignments";
|
||||
@import "utilities/colors";
|
||||
@import "utilities/size";
|
||||
@import "utilities/spacings";
|
||||
@import "utilities/transitions";
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: $scrollbar-thumb;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: $scrollbar-track;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: auto;
|
||||
overflow: visible;
|
||||
background: transparent;
|
||||
|
||||
/* inherit font, color & alignment from ancestor */
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: inherit;
|
||||
|
||||
/* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
|
||||
line-height: normal;
|
||||
|
||||
/* Corrects font smoothing for webkit */
|
||||
-webkit-font-smoothing: inherit;
|
||||
-moz-osx-font-smoothing: inherit;
|
||||
|
||||
/* Corrects inability to style clickable `input` types in iOS */
|
||||
-webkit-appearance: none;
|
||||
|
||||
/* Generalizes pointer cursor */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner {
|
||||
/* Remove excess padding and border in Firefox 4+ */
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Better accessibility for keyboard users */
|
||||
*:focus-visible {
|
||||
outline-style: auto !important;
|
||||
}
|
||||
|
||||
/** Utilities not covered by Bulma
|
||||
******************************************************************************/
|
||||
|
||||
|
||||
.tag.is-small {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.button.is-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.card.is-stretchable {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card.is-stretchable .card-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.preserve-whitespace p {
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
|
||||
.display-inline p {
|
||||
display: inline !important;
|
||||
}
|
||||
|
||||
button .button-invisible-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background: $invisible-overlay-background-color;
|
||||
color: white;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover .button-invisible-overlay,
|
||||
button:active .button-invisible-overlay,
|
||||
button:focus-visible .button-invisible-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
/** States
|
||||
******************************************************************************/
|
||||
|
||||
/* "disabled" for non-buttons */
|
||||
|
||||
.is-disabled {
|
||||
background-color: $pagination-disabled-background-color;
|
||||
border-color: $pagination-disabled-border-color;
|
||||
box-shadow: none;
|
||||
color: $pagination-disabled-color;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
/* Notifications page
|
||||
******************************************************************************/
|
||||
|
||||
.notification a.icon {
|
||||
text-decoration: none !important;
|
||||
}
|
7
bookwyrm/static/css/bookwyrm/components/_avatar.css
Normal file
7
bookwyrm/static/css/bookwyrm/components/_avatar.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
/** Avatars
|
||||
******************************************************************************/
|
||||
|
||||
.avatar {
|
||||
vertical-align: middle;
|
||||
display: inline;
|
||||
}
|
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;
|
||||
}
|
70
bookwyrm/static/css/bookwyrm/components/_book_cover.scss
Normal file
70
bookwyrm/static/css/bookwyrm/components/_book_cover.scss
Normal file
|
@ -0,0 +1,70 @@
|
|||
/** Book covers
|
||||
*
|
||||
* - .is-cover gives the behaviour of the cover and its surrounding. (optional)
|
||||
* - .cover-container gives the dimensions and position (for borders, image and other elements).
|
||||
* - .book-cover is positioned and sized based on its container.
|
||||
*
|
||||
* To have the cover within specific dimensions, specify a width or height for
|
||||
* standard bulma’s named breapoints:
|
||||
*
|
||||
* `is-(w|h)-(auto|xs|s|m|l|xl|xxl)[-(mobile|tablet|desktop)]`
|
||||
*
|
||||
* The cover will be centered horizontally and vertically within those dimensions.
|
||||
*
|
||||
* When using `.column.is-N`, add `.is-w-auto` to the container so that the flex
|
||||
* calculations are not biased by the default `max-content`.
|
||||
******************************************************************************/
|
||||
|
||||
.column.is-cover {
|
||||
flex-grow: 0 !important;
|
||||
}
|
||||
|
||||
.column.is-cover,
|
||||
.column.is-cover + .column {
|
||||
flex-basis: auto !important;
|
||||
}
|
||||
|
||||
.cover-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Book cover
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.book-cover {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
/* Useful when stretching under-sized images. */
|
||||
image-rendering: optimizequality;
|
||||
image-rendering: smooth;
|
||||
}
|
||||
|
||||
/* Cover caption
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.no-cover .cover-caption {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 0.5em;
|
||||
font-size: 0.75em;
|
||||
color: white;
|
||||
background-color: $no-cover-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
white-space: initial;
|
||||
text-align: center;
|
||||
}
|
36
bookwyrm/static/css/bookwyrm/components/_book_grid.scss
Normal file
36
bookwyrm/static/css/bookwyrm/components/_book_grid.scss
Normal file
|
@ -0,0 +1,36 @@
|
|||
/* Books grid
|
||||
******************************************************************************/
|
||||
|
||||
.books-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
align-items: end;
|
||||
justify-items: stretch;
|
||||
}
|
||||
|
||||
.books-grid > .is-big {
|
||||
grid-column: span 2;
|
||||
grid-row: span 2;
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
.books-grid .book-cover {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.books-grid .book-title {
|
||||
--height-basis: 1.35rem;
|
||||
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
line-height: var(--height-basis);
|
||||
min-height: calc(2 * var(--height-basis));
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 769px) {
|
||||
.books-grid {
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(8em, 1fr));
|
||||
}
|
||||
}
|
47
bookwyrm/static/css/bookwyrm/components/_book_list.scss
Normal file
47
bookwyrm/static/css/bookwyrm/components/_book_list.scss
Normal file
|
@ -0,0 +1,47 @@
|
|||
/* Book list
|
||||
******************************************************************************/
|
||||
|
||||
ol.ordered-list {
|
||||
list-style: none;
|
||||
counter-reset: list-counter;
|
||||
}
|
||||
|
||||
ol.ordered-list li {
|
||||
counter-increment: list-counter;
|
||||
}
|
||||
|
||||
ol.ordered-list li::before {
|
||||
content: counter(list-counter);
|
||||
position: absolute;
|
||||
left: -20px;
|
||||
width: 20px;
|
||||
height: 24px;
|
||||
background-color: $scheme-main;
|
||||
border: 1px solid $border;
|
||||
border-right: 0;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: $text-light;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
ol.ordered-list li::before {
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
border: 0;
|
||||
border-right: 1px solid $border;
|
||||
border-bottom: 1px solid $border;
|
||||
border-radius: 0;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.overflow-wrap-anywhere {
|
||||
overflow-wrap: anywhere;
|
||||
min-width: 10em;
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/* Book preview table
|
||||
******************************************************************************/
|
||||
|
||||
.book-preview td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
table.is-mobile,
|
||||
table.is-mobile tbody {
|
||||
display: block;
|
||||
}
|
||||
|
||||
table.is-mobile tr {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid $border;
|
||||
}
|
||||
|
||||
table.is-mobile td {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
flex: 1 0 100%;
|
||||
order: 2;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
table.is-mobile td.book-preview-top-row {
|
||||
order: 1;
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
table.is-mobile td[data-title]:not(:empty)::before {
|
||||
content: attr(data-title);
|
||||
display: block;
|
||||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
table.is-mobile td:empty {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table.is-mobile th,
|
||||
table.is-mobile thead {
|
||||
display: none;
|
||||
}
|
||||
}
|
13
bookwyrm/static/css/bookwyrm/components/_breadcrumbs.scss
Normal file
13
bookwyrm/static/css/bookwyrm/components/_breadcrumbs.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
/* Breadcrumbs
|
||||
******************************************************************************/
|
||||
|
||||
.breadcrumb li:first-child * {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.breadcrumb li > * {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0 0.75em;
|
||||
}
|
30
bookwyrm/static/css/bookwyrm/components/_copy.scss
Normal file
30
bookwyrm/static/css/bookwyrm/components/_copy.scss
Normal file
|
@ -0,0 +1,30 @@
|
|||
/* Copy
|
||||
******************************************************************************/
|
||||
|
||||
.horizontal-copy {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.horizontal-copy textarea {
|
||||
min-width: initial;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.horizontal-copy button {
|
||||
align-self: stretch;
|
||||
height: unset;
|
||||
}
|
||||
|
||||
.vertical-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.vertical-copy button {
|
||||
width: 100%;
|
||||
}
|
116
bookwyrm/static/css/bookwyrm/components/_details.scss
Normal file
116
bookwyrm/static/css/bookwyrm/components/_details.scss
Normal file
|
@ -0,0 +1,116 @@
|
|||
/** General `details` element styles
|
||||
******************************************************************************/
|
||||
|
||||
details summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
details summary::marker {
|
||||
content: none;
|
||||
}
|
||||
|
||||
details.detail-pinned-button summary {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
details.detail-pinned-button form {
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
/** Dropdown w/ Details element
|
||||
******************************************************************************/
|
||||
|
||||
details.dropdown[open] summary.dropdown-trigger::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
details.dropdown .dropdown-menu {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
details.dropdown .dropdown-menu button {
|
||||
/* Fix weird Safari defaults */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
details.dropdown .dropdown-menu button:focus-visible,
|
||||
details.dropdown .dropdown-menu a:focus-visible {
|
||||
outline-style: auto;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
details.dropdown[open] summary.dropdown-trigger::before {
|
||||
background-color: $modal-background-background-color;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
details .dropdown-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
details .dropdown-menu > * {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
/** Details panel
|
||||
******************************************************************************/
|
||||
|
||||
details.details-panel {
|
||||
box-shadow: 0 0 0 1px $border;
|
||||
transition: box-shadow 0.2s ease;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
details[open].details-panel,
|
||||
details.details-panel:hover {
|
||||
box-shadow: 0 0 0 1px $border;
|
||||
}
|
||||
|
||||
details.details-panel summary {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
details summary .details-close {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transform: rotate(45deg);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
details[open] summary .details-close {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 769px) {
|
||||
.details-panel .filters-field:not(:last-child) {
|
||||
border-right: 1px solid $border;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
}
|
28
bookwyrm/static/css/bookwyrm/components/_file_input.scss
Normal file
28
bookwyrm/static/css/bookwyrm/components/_file_input.scss
Normal file
|
@ -0,0 +1,28 @@
|
|||
/** File input styles
|
||||
******************************************************************************/
|
||||
|
||||
input[type="file"]::file-selector-button {
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-color: $scheme-main;
|
||||
border-radius: 4px;
|
||||
border: 1px solid $border;
|
||||
box-shadow: none;
|
||||
color: $text;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
height: 2.5em;
|
||||
justify-content: center;
|
||||
line-height: 1.5;
|
||||
padding-bottom: calc(0.5em - 1px);
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
padding-top: calc(0.5em - 1px);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
input[type="file"]::file-selector-button:hover {
|
||||
border-color: $border-hover;
|
||||
color: text;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/** Transient notification
|
||||
******************************************************************************/
|
||||
|
||||
#live-messages {
|
||||
position: fixed;
|
||||
bottom: 1em;
|
||||
right: 1em;
|
||||
}
|
10
bookwyrm/static/css/bookwyrm/components/_shelving.scss
Normal file
10
bookwyrm/static/css/bookwyrm/components/_shelving.scss
Normal file
|
@ -0,0 +1,10 @@
|
|||
/** Shelving
|
||||
******************************************************************************/
|
||||
|
||||
/** @todo Replace icons with SVG symbols.
|
||||
@see https://www.youtube.com/watch?v=9xXBYcWgCHA */
|
||||
.shelf-option:disabled > *::after {
|
||||
font-family: icomoon; /* stylelint-disable font-family-no-missing-generic-family-keyword */
|
||||
content: "\e919"; /* icon-check */
|
||||
margin-left: 0.5em;
|
||||
}
|
52
bookwyrm/static/css/bookwyrm/components/_stars.scss
Normal file
52
bookwyrm/static/css/bookwyrm/components/_stars.scss
Normal file
|
@ -0,0 +1,52 @@
|
|||
/** Stars
|
||||
******************************************************************************/
|
||||
|
||||
.stars {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/** Stars in a review form
|
||||
*
|
||||
* Specificity makes hovering taking over checked inputs.
|
||||
*
|
||||
* \e9d9: filled star
|
||||
* \e9d7: empty star;
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.form-rate-stars {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
/* All stars are visually filled by default. */
|
||||
.form-rate-stars .icon::before {
|
||||
content: "\e9d9"; /* icon-star-full */
|
||||
}
|
||||
|
||||
/* Icons directly following half star inputs are marked as half */
|
||||
.form-rate-stars input.half:checked ~ .icon::before {
|
||||
content: "\e9d8"; /* icon-star-half */
|
||||
}
|
||||
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
.form-rate-stars input.half:checked + input + .icon:hover::before {
|
||||
content: "\e9d8" !important; /* icon-star-half */
|
||||
}
|
||||
|
||||
/* Icons directly following half check inputs that follow the checked input are emptied. */
|
||||
.form-rate-stars input.half:checked + input + .icon ~ .icon::before {
|
||||
content: "\e9d7"; /* icon-star-empty */
|
||||
}
|
||||
|
||||
/* Icons directly following inputs that follow the checked input are emptied. */
|
||||
.form-rate-stars input:checked ~ input + .icon::before {
|
||||
content: "\e9d7"; /* icon-star-empty */
|
||||
}
|
||||
|
||||
/* When a label is hovered, repeat the fill-all-then-empty-following pattern. */
|
||||
.form-rate-stars:hover .icon.icon::before {
|
||||
content: "\e9d9" !important; /* icon-star-full */
|
||||
}
|
||||
|
||||
.form-rate-stars .icon:hover ~ .icon::before {
|
||||
content: "\e9d7" !important; /* icon-star-empty */
|
||||
}
|
57
bookwyrm/static/css/bookwyrm/components/_status.scss
Normal file
57
bookwyrm/static/css/bookwyrm/components/_status.scss
Normal file
|
@ -0,0 +1,57 @@
|
|||
/** Statuses: Quotes
|
||||
*
|
||||
* \e906: icon-quote-open
|
||||
* \e905: icon-quote-close
|
||||
*
|
||||
* The `content` class on the blockquote allows to apply styles to markdown
|
||||
* generated HTML in the quote: https://bulma.io/documentation/elements/content/
|
||||
*
|
||||
* ```html
|
||||
* <div class="quote block">
|
||||
* <blockquote dir="auto" class="content mb-2">
|
||||
* User generated quote in markdown…
|
||||
* </blockquote>
|
||||
*
|
||||
* <p> — <a…>Book Title</a> by <a…class="author">Author</a></p>
|
||||
* </div>
|
||||
* ```
|
||||
******************************************************************************/
|
||||
|
||||
.quote > blockquote {
|
||||
position: relative;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.quote > blockquote::before,
|
||||
.quote > blockquote::after {
|
||||
font-family: icomoon; /* stylelint-disable font-family-no-missing-generic-family-keyword */
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.quote > blockquote::before {
|
||||
content: "\e907"; /* icon-quote-open */
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.quote > blockquote::after {
|
||||
content: "\e906"; /* icon-quote-close */
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* Threads
|
||||
******************************************************************************/
|
||||
|
||||
.thread .is-main .card {
|
||||
box-shadow: 0 0.5em 1em -0.125em rgba($link, 0.35), 0 0 0 1px rgba($link, 0.02);
|
||||
}
|
||||
|
||||
.thread::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 2.5em;
|
||||
border-left: 2px solid $border;
|
||||
}
|
176
bookwyrm/static/css/bookwyrm/components/_tabs.scss
Normal file
176
bookwyrm/static/css/bookwyrm/components/_tabs.scss
Normal file
|
@ -0,0 +1,176 @@
|
|||
/** Bookwyrm Tabs
|
||||
******************************************************************************/
|
||||
|
||||
.bw-tabs {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-webkit-touch-callout: none;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: 1rem;
|
||||
justify-content: flex-start;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bw-tabs::before {
|
||||
border-bottom-color: $border;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
bottom: 0;
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bw-tabs:not(:last-child) {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.bw-tabs a {
|
||||
align-items: center;
|
||||
border-bottom-color: $border;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
color: $text;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: -1px;
|
||||
padding: 0.5em 1em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bw-tabs a:hover {
|
||||
border-bottom-color: transparent;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.bw-tabs a.is-active {
|
||||
border-bottom-color: transparent;
|
||||
color: $link;
|
||||
}
|
||||
|
||||
.bw-tabs.is-left {
|
||||
padding-right: 0.75em;
|
||||
}
|
||||
|
||||
.bw-tabs.is-center {
|
||||
flex: none;
|
||||
justify-content: center;
|
||||
padding-left: 0.75em;
|
||||
padding-right: 0.75em;
|
||||
}
|
||||
|
||||
.bw-tabs.is-right {
|
||||
justify-content: flex-end;
|
||||
padding-left: 0.75em;
|
||||
}
|
||||
|
||||
.bw-tabs .icon:first-child {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.bw-tabs .icon:last-child {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.bw-tabs.is-centered {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bw-tabs.is-boxed a {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.bw-tabs.is-boxed a:hover {
|
||||
background-color: $background-secondary;
|
||||
border-bottom-color: $border-hover;
|
||||
}
|
||||
|
||||
.bw-tabs.is-boxed a.is-active {
|
||||
background-color: $background-body;
|
||||
border-color: $border;
|
||||
border-bottom-color: $border !important;
|
||||
}
|
||||
|
||||
.bw-tabs.is-fullwidth a {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bw-tabs.is-toggle a {
|
||||
border-color: $border;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
margin-bottom: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bw-tabs.is-toggle a:hover {
|
||||
background-color: $background-secondary;
|
||||
border-color: $border;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.bw-tabs.is-toggle a + a {
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.bw-tabs.is-toggle a:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.bw-tabs.is-toggle a:last-child {
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.bw-tabs.is-toggle a.is-active {
|
||||
background-color: $link-background;
|
||||
border-color: $link;
|
||||
color: $text;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bw-tabs.is-toggle {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.bw-tabs.is-toggle.is-toggle-rounded a:first-child {
|
||||
border-bottom-left-radius: 290486px;
|
||||
border-top-left-radius: 290486px;
|
||||
padding-left: 1.25em;
|
||||
}
|
||||
|
||||
.bw-tabs.is-toggle.is-toggle-rounded a:last-child {
|
||||
border-bottom-right-radius: 290486px;
|
||||
border-top-right-radius: 290486px;
|
||||
padding-right: 1.25em;
|
||||
}
|
||||
|
||||
.bw-tabs.is-small {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.bw-tabs.is-medium {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.bw-tabs.is-large {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.bw-tabs.has-aside-text a {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.bw-tabs a .aside-text {
|
||||
position: absolute;
|
||||
top: calc(-0.75rem - 0.75rem);
|
||||
left: 0;
|
||||
color: $text;
|
||||
}
|
45
bookwyrm/static/css/bookwyrm/components/_toggle.scss
Normal file
45
bookwyrm/static/css/bookwyrm/components/_toggle.scss
Normal file
|
@ -0,0 +1,45 @@
|
|||
/** Toggles
|
||||
******************************************************************************/
|
||||
|
||||
.toggle-button[aria-pressed="true"],
|
||||
.toggle-button[aria-pressed="true"]:hover {
|
||||
background-color: $primary;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hide-active[aria-pressed="true"],
|
||||
.hide-inactive[aria-pressed="false"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.transition-x.is-hidden,
|
||||
.transition-y.is-hidden {
|
||||
display: block !important;
|
||||
visibility: hidden !important;
|
||||
height: 0 !important;
|
||||
width: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.transition-x,
|
||||
.transition-y {
|
||||
transition-duration: 0.5s;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
.transition-x {
|
||||
transition-property: width, margin-left, margin-right, padding-left, padding-right;
|
||||
}
|
||||
|
||||
.transition-y {
|
||||
transition-property: height, margin-top, margin-bottom, padding-top, padding-bottom;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.transition-x,
|
||||
.transition-y {
|
||||
transition-duration: 0.001ms !important;
|
||||
}
|
||||
}
|
61
bookwyrm/static/css/bookwyrm/overrides/_bulma_overrides.scss
Normal file
61
bookwyrm/static/css/bookwyrm/overrides/_bulma_overrides.scss
Normal file
|
@ -0,0 +1,61 @@
|
|||
.image {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.navbar .logo {
|
||||
max-height: 50px;
|
||||
}
|
||||
|
||||
.card {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.card.has-border {
|
||||
border: 1px solid $border;
|
||||
}
|
||||
|
||||
.scroll-x {
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.modal-card > * {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
.modal-card:focus {
|
||||
outline-style: auto;
|
||||
}
|
||||
|
||||
.modal-card:focus:not(:focus-visible) {
|
||||
outline-style: initial;
|
||||
}
|
||||
|
||||
.modal-card:focus-visible {
|
||||
outline-style: auto;
|
||||
}
|
||||
/* stylelint-enable no-descending-specificity */
|
||||
|
||||
.modal-card.is-fullwidth {
|
||||
min-width: 75% !important;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 769px) {
|
||||
.modal-card.is-thin {
|
||||
width: 350px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-card-body {
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.clip-text {
|
||||
max-height: 35em;
|
||||
overflow: hidden;
|
||||
}
|
33
bookwyrm/static/css/bookwyrm/utilities/_a11y.scss
Normal file
33
bookwyrm/static/css/bookwyrm/utilities/_a11y.scss
Normal file
|
@ -0,0 +1,33 @@
|
|||
@media only screen and (max-width: 768px) {
|
||||
.is-sr-only-mobile {
|
||||
border: none !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
height: 0.01em !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
position: absolute !important;
|
||||
white-space: nowrap !important;
|
||||
width: 0.01em !important;
|
||||
}
|
||||
|
||||
.m-0-mobile {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.card-footer.is-stacked-mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-footer.is-stacked-mobile .card-footer-item:not(:last-child) {
|
||||
border-bottom: 1px solid $background-tertiary;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.is-flex-direction-row-mobile {
|
||||
flex-direction: row !important;
|
||||
}
|
||||
|
||||
.is-flex-direction-column-mobile {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
}
|
76
bookwyrm/static/css/bookwyrm/utilities/_alignments.scss
Normal file
76
bookwyrm/static/css/bookwyrm/utilities/_alignments.scss
Normal file
|
@ -0,0 +1,76 @@
|
|||
/* Alignments
|
||||
*
|
||||
* Use them with `.align.to-(c|t|r|b|l)[-(mobile|tablet)]`
|
||||
******************************************************************************/
|
||||
|
||||
/* Flex item position
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.align {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
}
|
||||
|
||||
.align.to-c {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.align.to-t {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.align.to-r {
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
|
||||
.align.to-b {
|
||||
align-items: flex-end !important;
|
||||
}
|
||||
|
||||
.align.to-l {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.align.to-c-mobile {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.align.to-t-mobile {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.align.to-r-mobile {
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
|
||||
.align.to-b-mobile {
|
||||
align-items: flex-end !important;
|
||||
}
|
||||
|
||||
.align.to-l-mobile {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
.align.to-c-tablet {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.align.to-t-tablet {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.align.to-r-tablet {
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
|
||||
.align.to-b-tablet {
|
||||
align-items: flex-end !important;
|
||||
}
|
||||
|
||||
.align.to-l-tablet {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
}
|
25
bookwyrm/static/css/bookwyrm/utilities/_colors.scss
Normal file
25
bookwyrm/static/css/bookwyrm/utilities/_colors.scss
Normal file
|
@ -0,0 +1,25 @@
|
|||
/* Semantic color classes */
|
||||
|
||||
.has-background-primary-highlight {
|
||||
background-color: $primary-highlight;
|
||||
}
|
||||
|
||||
.has-background-info-highlight {
|
||||
background-color: $info-highlight;
|
||||
}
|
||||
|
||||
.has-background-success-highlight {
|
||||
background-color: $success-highlight;
|
||||
}
|
||||
|
||||
.has-background-body {
|
||||
background-color: $background-body;
|
||||
}
|
||||
|
||||
.has-background-secondary {
|
||||
background-color: $background-secondary !important;
|
||||
}
|
||||
|
||||
.has-background-tertiary {
|
||||
background-color: $background-tertiary !important;
|
||||
}
|
227
bookwyrm/static/css/bookwyrm/utilities/_size.scss
Normal file
227
bookwyrm/static/css/bookwyrm/utilities/_size.scss
Normal file
|
@ -0,0 +1,227 @@
|
|||
/* Dimensions
|
||||
* @todo These could be in rem.
|
||||
******************************************************************************/
|
||||
|
||||
.is-32x32 {
|
||||
min-width: 32px !important;
|
||||
min-height: 32px !important;
|
||||
}
|
||||
|
||||
.is-96x96 {
|
||||
min-width: 96px !important;
|
||||
min-height: 96px !important;
|
||||
}
|
||||
|
||||
.is-w-auto {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.is-w-xs {
|
||||
width: 80px !important;
|
||||
}
|
||||
|
||||
.is-w-s {
|
||||
width: 100px !important;
|
||||
}
|
||||
|
||||
.is-w-m {
|
||||
width: 150px !important;
|
||||
}
|
||||
|
||||
.is-w-l {
|
||||
width: 200px !important;
|
||||
}
|
||||
|
||||
.is-w-xl {
|
||||
width: 250px !important;
|
||||
}
|
||||
|
||||
.is-w-xxl {
|
||||
width: 500px !important;
|
||||
}
|
||||
|
||||
.is-h-xs {
|
||||
height: 80px !important;
|
||||
}
|
||||
|
||||
.is-h-s {
|
||||
height: 100px !important;
|
||||
}
|
||||
|
||||
.is-h-m {
|
||||
height: 150px !important;
|
||||
}
|
||||
|
||||
.is-h-l {
|
||||
height: 200px !important;
|
||||
}
|
||||
|
||||
.is-h-xl {
|
||||
height: 250px !important;
|
||||
}
|
||||
|
||||
.is-h-xxl {
|
||||
height: 500px !important;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.is-w-auto-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.is-w-xs-mobile {
|
||||
width: 80px !important;
|
||||
}
|
||||
|
||||
.is-w-s-mobile {
|
||||
width: 100px !important;
|
||||
}
|
||||
|
||||
.is-w-m-mobile {
|
||||
width: 150px !important;
|
||||
}
|
||||
|
||||
.is-w-l-mobile {
|
||||
width: 200px !important;
|
||||
}
|
||||
|
||||
.is-w-xl-mobile {
|
||||
width: 250px !important;
|
||||
}
|
||||
|
||||
.is-w-xxl-mobile {
|
||||
width: 500px !important;
|
||||
}
|
||||
|
||||
.is-h-xs-mobile {
|
||||
height: 80px !important;
|
||||
}
|
||||
|
||||
.is-h-s-mobile {
|
||||
height: 100px !important;
|
||||
}
|
||||
|
||||
.is-h-m-mobile {
|
||||
height: 150px !important;
|
||||
}
|
||||
|
||||
.is-h-l-mobile {
|
||||
height: 200px !important;
|
||||
}
|
||||
|
||||
.is-h-xl-mobile {
|
||||
height: 250px !important;
|
||||
}
|
||||
|
||||
.is-h-xxl-mobile {
|
||||
height: 500px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 769px) {
|
||||
.is-w-auto-tablet {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.is-w-xs-tablet {
|
||||
width: 80px !important;
|
||||
}
|
||||
|
||||
.is-w-s-tablet {
|
||||
width: 100px !important;
|
||||
}
|
||||
|
||||
.is-w-m-tablet {
|
||||
width: 150px !important;
|
||||
}
|
||||
|
||||
.is-w-l-tablet {
|
||||
width: 200px !important;
|
||||
}
|
||||
|
||||
.is-w-xl-tablet {
|
||||
width: 250px !important;
|
||||
}
|
||||
|
||||
.is-w-xxl-tablet {
|
||||
width: 500px !important;
|
||||
}
|
||||
|
||||
.is-h-xs-tablet {
|
||||
height: 80px !important;
|
||||
}
|
||||
|
||||
.is-h-s-tablet {
|
||||
height: 100px !important;
|
||||
}
|
||||
|
||||
.is-h-m-tablet {
|
||||
height: 150px !important;
|
||||
}
|
||||
|
||||
.is-h-l-tablet {
|
||||
height: 200px !important;
|
||||
}
|
||||
|
||||
.is-h-xl-tablet {
|
||||
height: 250px !important;
|
||||
}
|
||||
|
||||
.is-h-xxl-tablet {
|
||||
height: 500px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1024px) {
|
||||
.is-w-auto-desktop {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.is-w-xs-desktop {
|
||||
width: 80px !important;
|
||||
}
|
||||
|
||||
.is-w-s-desktop {
|
||||
width: 100px !important;
|
||||
}
|
||||
|
||||
.is-w-m-desktop {
|
||||
width: 150px !important;
|
||||
}
|
||||
|
||||
.is-w-l-desktop {
|
||||
width: 200px !important;
|
||||
}
|
||||
|
||||
.is-w-xl-desktop {
|
||||
width: 250px !important;
|
||||
}
|
||||
|
||||
.is-w-xxl-desktop {
|
||||
width: 500px !important;
|
||||
}
|
||||
|
||||
.is-h-xs-desktop {
|
||||
height: 80px !important;
|
||||
}
|
||||
|
||||
.is-h-s-desktop {
|
||||
height: 100px !important;
|
||||
}
|
||||
|
||||
.is-h-m-desktop {
|
||||
height: 150px !important;
|
||||
}
|
||||
|
||||
.is-h-l-desktop {
|
||||
height: 200px !important;
|
||||
}
|
||||
|
||||
.is-h-xl-desktop {
|
||||
height: 250px !important;
|
||||
}
|
||||
|
||||
.is-h-xxl-desktop {
|
||||
height: 500px !important;
|
||||
}
|
||||
}
|
167
bookwyrm/static/css/bookwyrm/utilities/_spacings.scss
Normal file
167
bookwyrm/static/css/bookwyrm/utilities/_spacings.scss
Normal file
|
@ -0,0 +1,167 @@
|
|||
/* Spacings
|
||||
*
|
||||
* Those are supplementary rules to Bulma’s. They follow the same conventions.
|
||||
* Add those you’ll need.
|
||||
******************************************************************************/
|
||||
|
||||
.mr-auto {
|
||||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
.ml-auto {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.m-0-mobile {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.mr-auto-mobile {
|
||||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
.ml-auto-mobile {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
.mt-3-mobile {
|
||||
margin-top: 0.75rem !important;
|
||||
}
|
||||
|
||||
.ml-3-mobile {
|
||||
margin-left: 0.75rem !important;
|
||||
}
|
||||
|
||||
.mx-3-mobile {
|
||||
margin-right: 0.75rem !important;
|
||||
margin-left: 0.75rem !important;
|
||||
}
|
||||
|
||||
.my-3-mobile {
|
||||
margin-top: 0.75rem !important;
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
.m-0-tablet {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.mr-auto-tablet {
|
||||
margin-right: auto !important;
|
||||
}
|
||||
|
||||
.ml-auto-tablet {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
.mt-3-tablet {
|
||||
margin-top: 0.75rem !important;
|
||||
}
|
||||
|
||||
.ml-3-tablet {
|
||||
margin-left: 0.75rem !important;
|
||||
}
|
||||
|
||||
.mx-3-tablet {
|
||||
margin-right: 0.75rem !important;
|
||||
margin-left: 0.75rem !important;
|
||||
}
|
||||
|
||||
.my-3-tablet {
|
||||
margin-top: 0.75rem !important;
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Gaps (for Flexbox and Grid)
|
||||
*
|
||||
* Those are supplementary rules to Bulma’s. They follow the same conventions.
|
||||
* Add those you’ll need.
|
||||
******************************************************************************/
|
||||
|
||||
.is-gap-0 {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.is-gap-1 {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.is-gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.is-gap-3 {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.is-gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.is-gap-5 {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.is-gap-6 {
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.is-row-gap-0 {
|
||||
row-gap: 0;
|
||||
}
|
||||
|
||||
.is-row-gap-1 {
|
||||
row-gap: 0.25rem;
|
||||
}
|
||||
|
||||
.is-row-gap-2 {
|
||||
row-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.is-row-gap-3 {
|
||||
row-gap: 0.75rem;
|
||||
}
|
||||
|
||||
.is-row-gap-4 {
|
||||
row-gap: 1rem;
|
||||
}
|
||||
|
||||
.is-row-gap-5 {
|
||||
row-gap: 1.5rem;
|
||||
}
|
||||
|
||||
.is-row-gap-6 {
|
||||
row-gap: 3rem;
|
||||
}
|
||||
|
||||
.is-column-gap-0 {
|
||||
column-gap: 0;
|
||||
}
|
||||
|
||||
.is-column-gap-1 {
|
||||
column-gap: 0.25rem;
|
||||
}
|
||||
|
||||
.is-column-gap-2 {
|
||||
column-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.is-column-gap-3 {
|
||||
column-gap: 0.75rem;
|
||||
}
|
||||
|
||||
.is-column-gap-4 {
|
||||
column-gap: 1rem;
|
||||
}
|
||||
|
||||
.is-column-gap-5 {
|
||||
column-gap: 1.5rem;
|
||||
}
|
||||
|
||||
.is-column-gap-6 {
|
||||
column-gap: 3rem;
|
||||
}
|
25
bookwyrm/static/css/bookwyrm/utilities/_transitions.scss
Normal file
25
bookwyrm/static/css/bookwyrm/utilities/_transitions.scss
Normal file
|
@ -0,0 +1,25 @@
|
|||
/** Animations and transitions
|
||||
******************************************************************************/
|
||||
|
||||
@keyframes turning {
|
||||
from { transform: rotateZ(0deg); }
|
||||
to { transform: rotateZ(360deg); }
|
||||
}
|
||||
|
||||
.is-processing .icon-spinner::before {
|
||||
animation: turning 1.5s infinite linear;
|
||||
}
|
||||
|
||||
.icon-spinner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.is-processing .icon-spinner {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.is-processing .icon::before {
|
||||
transition-duration: 0.001ms !important;
|
||||
}
|
||||
}
|
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.
88
bookwyrm/static/css/themes/bookwyrm-dark.scss
Normal file
88
bookwyrm/static/css/themes/bookwyrm-dark.scss
Normal file
|
@ -0,0 +1,88 @@
|
|||
@import "../vendor/bulma/sass/utilities/initial-variables.sass";
|
||||
|
||||
/* Colors
|
||||
******************************************************************************/
|
||||
|
||||
/* states */
|
||||
$primary: #005e50;
|
||||
$primary-light: #1d2b28;
|
||||
$info: #1f4666;
|
||||
$success: #246447;
|
||||
$success-light: #0d2f1e;
|
||||
$warning: #8b6c15;
|
||||
$warning-light: #372e13;
|
||||
$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);
|
||||
$scrollbar-track: $background-secondary;
|
||||
$scrollbar-thumb: $light;
|
||||
|
||||
/* 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";
|
61
bookwyrm/static/css/themes/bookwyrm-light.scss
Normal file
61
bookwyrm/static/css/themes/bookwyrm-light.scss
Normal file
|
@ -0,0 +1,61 @@
|
|||
@import "../vendor/bulma/sass/utilities/derived-variables.sass";
|
||||
|
||||
/* Colors
|
||||
******************************************************************************/
|
||||
|
||||
/* states */
|
||||
$primary: $turquoise;
|
||||
$info: $cyan;
|
||||
$success: $green;
|
||||
$warning: $yellow;
|
||||
$danger: $red;
|
||||
|
||||
/* book cover standins */
|
||||
$no-cover-color: #002549;
|
||||
|
||||
/* background colors */
|
||||
$scheme-main: $white;
|
||||
$scheme-main: $white-bis;
|
||||
$background-body: $white;
|
||||
$background-secondary: $white-ter;
|
||||
$background-tertiary: $white-bis;
|
||||
$scrollbar-track: $background-secondary;
|
||||
$scrollbar-thumb: $grey-lighter;
|
||||
|
||||
/* highlight colors */
|
||||
$primary-highlight: $primary-light;
|
||||
$info-highlight: $info-light;
|
||||
$success-highlight: $success-light;
|
||||
|
||||
/* borders */
|
||||
$border: $grey-lighter;
|
||||
$border-hover: $grey-light;
|
||||
$border-light: $grey-lightest;
|
||||
$border-light-hover: $grey-light;
|
||||
|
||||
/* text */
|
||||
$text: $grey-dark;
|
||||
$text-light: $grey;
|
||||
$text-strong: $grey-darker;
|
||||
|
||||
/* links */
|
||||
$link: #3273dc;
|
||||
$link-background: $link;
|
||||
$link-hover: $grey-darker;
|
||||
$link-focus: $grey-darker;
|
||||
$link-active: $grey-darker;
|
||||
|
||||
/* bulma overrides */
|
||||
$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";
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue