diff --git a/.env.example b/.env.example
index 58c53b5bf..bbd825a9a 100644
--- a/.env.example
+++ b/.env.example
@@ -108,3 +108,10 @@ OTEL_EXPORTER_OTLP_ENDPOINT=
OTEL_EXPORTER_OTLP_HEADERS=
# Service name to identify your app
OTEL_SERVICE_NAME=
+
+# Set HTTP_X_FORWARDED_PROTO ONLY to true if you know what you are doing.
+# Only use it if your proxy is "swallowing" if the original request was made
+# via https. Please refer to the Django-Documentation and assess the risks
+# for your instance:
+# https://docs.djangoproject.com/en/3.2/ref/settings/#secure-proxy-ssl-header
+HTTP_X_FORWARDED_PROTO=false
diff --git a/.github/workflows/django-tests.yml b/.github/workflows/django-tests.yml
index 97a744813..4335a4605 100644
--- a/.github/workflows/django-tests.yml
+++ b/.github/workflows/django-tests.yml
@@ -56,5 +56,6 @@ jobs:
EMAIL_USE_TLS: true
ENABLE_PREVIEW_IMAGES: false
ENABLE_THUMBNAIL_GENERATION: true
+ HTTP_X_FORWARDED_PROTO: false
run: |
pytest -n 3
diff --git a/README.md b/README.md
index 558d42d45..f8b2eb1f6 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@ Keep track of what books you've read, and what books you'd like to read in the f
Federation allows you to interact with users on other instances and services, and also shares metadata about books and authors, which collaboratively builds a decentralized database of books.
### Privacy and moderation
-Users and administrators can control who can see thier posts and what other instances to federate with.
+Users and administrators can control who can see their posts and what other instances to federate with.
## Tech Stack
Web backend
diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py
index e942c9aeb..095ec0227 100644
--- a/bookwyrm/activitypub/base_activity.py
+++ b/bookwyrm/activitypub/base_activity.py
@@ -194,6 +194,11 @@ class ActivityObject:
try:
if issubclass(type(v), ActivityObject):
data[k] = v.serialize()
+ elif isinstance(v, list):
+ data[k] = [
+ e.serialize() if issubclass(type(e), ActivityObject) else e
+ for e in v
+ ]
except TypeError:
pass
data = {k: v for (k, v) in data.items() if v is not None and k not in omit}
@@ -271,7 +276,7 @@ def resolve_remote_id(
try:
data = get_data(remote_id)
except ConnectorException:
- logger.exception("Could not connect to host for remote_id: %s", remote_id)
+ logger.info("Could not connect to host for remote_id: %s", remote_id)
return None
# determine the model implicitly, if not provided
@@ -306,7 +311,9 @@ class Link(ActivityObject):
def serialize(self, **kwargs):
"""remove fields"""
- omit = ("id", "type", "@context")
+ omit = ("id", "@context")
+ if self.type == "Link":
+ omit += ("type",)
return super().serialize(omit=omit)
diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py
index c1ee7fe78..8ae93926a 100644
--- a/bookwyrm/connectors/abstract_connector.py
+++ b/bookwyrm/connectors/abstract_connector.py
@@ -222,7 +222,7 @@ def dict_from_mappings(data, mappings):
return result
-def get_data(url, params=None, timeout=10):
+def get_data(url, params=None, timeout=settings.QUERY_TIMEOUT):
"""wrapper for request.get"""
# check if the url is blocked
raise_not_valid_url(url)
diff --git a/bookwyrm/connectors/inventaire.py b/bookwyrm/connectors/inventaire.py
index df9b2e43a..a330b2c4a 100644
--- a/bookwyrm/connectors/inventaire.py
+++ b/bookwyrm/connectors/inventaire.py
@@ -165,8 +165,8 @@ class Connector(AbstractConnector):
edition_data = self.get_book_data(edition_data)
except ConnectorException:
# who, indeed, knows
- return
- super().create_edition_from_data(work, edition_data, instance=instance)
+ return None
+ return super().create_edition_from_data(work, edition_data, instance=instance)
def get_cover_url(self, cover_blob, *_):
"""format the relative cover url into an absolute one:
diff --git a/bookwyrm/emailing.py b/bookwyrm/emailing.py
index 80aacf7f4..03cf4772e 100644
--- a/bookwyrm/emailing.py
+++ b/bookwyrm/emailing.py
@@ -38,7 +38,7 @@ def password_reset_email(reset_code):
data = email_data()
data["reset_link"] = reset_code.link
data["user"] = reset_code.user.display_name
- send_email.delay(reset_code.user.email, *format_email("password_reset", data))
+ send_email(reset_code.user.email, *format_email("password_reset", data))
def moderation_report_email(report):
diff --git a/bookwyrm/forms/admin.py b/bookwyrm/forms/admin.py
index ae15e011b..2d69ef702 100644
--- a/bookwyrm/forms/admin.py
+++ b/bookwyrm/forms/admin.py
@@ -55,7 +55,7 @@ class CreateInviteForm(CustomForm):
class SiteForm(CustomForm):
class Meta:
model = models.SiteSettings
- exclude = ["admin_code", "install_mode"]
+ exclude = ["admin_code", "install_mode", "imports_enabled"]
widgets = {
"instance_short_description": forms.TextInput(
attrs={"aria-describedby": "desc_instance_short_description"}
diff --git a/bookwyrm/forms/links.py b/bookwyrm/forms/links.py
index de229bc2d..d2fd5f116 100644
--- a/bookwyrm/forms/links.py
+++ b/bookwyrm/forms/links.py
@@ -36,13 +36,16 @@ class FileLinkForm(CustomForm):
"This domain is blocked. Please contact your administrator if you think this is an error."
),
)
- elif models.FileLink.objects.filter(
+ if (
+ not self.instance
+ and 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."
- ),
- )
+ ).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."
+ ),
+ )
diff --git a/bookwyrm/importers/importer.py b/bookwyrm/importers/importer.py
index a3cfba198..e4ee2c31a 100644
--- a/bookwyrm/importers/importer.py
+++ b/bookwyrm/importers/importer.py
@@ -16,8 +16,8 @@ class Importer:
("id", ["id", "book id"]),
("title", ["title"]),
("authors", ["author", "authors", "primary author"]),
- ("isbn_10", ["isbn10", "isbn"]),
- ("isbn_13", ["isbn13", "isbn", "isbns"]),
+ ("isbn_10", ["isbn10", "isbn", "isbn/uid"]),
+ ("isbn_13", ["isbn13", "isbn", "isbns", "isbn/uid"]),
("shelf", ["shelf", "exclusive shelf", "read status", "bookshelf"]),
("review_name", ["review name"]),
("review_body", ["my review", "review"]),
@@ -36,7 +36,11 @@ class Importer:
def create_job(self, user, csv_file, include_reviews, privacy):
"""check over a csv and creates a database entry for the job"""
csv_reader = csv.DictReader(csv_file, delimiter=self.delimiter)
- rows = enumerate(list(csv_reader))
+ rows = list(csv_reader)
+ if len(rows) < 1:
+ raise ValueError("CSV file is empty")
+ rows = enumerate(rows)
+
job = ImportJob.objects.create(
user=user,
include_reviews=include_reviews,
diff --git a/bookwyrm/migrations/0166_sitesettings_imports_enabled.py b/bookwyrm/migrations/0166_sitesettings_imports_enabled.py
new file mode 100644
index 000000000..ccf4ef374
--- /dev/null
+++ b/bookwyrm/migrations/0166_sitesettings_imports_enabled.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.16 on 2022-11-17 21:50
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("bookwyrm", "0165_alter_inviterequest_answer"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="sitesettings",
+ name="imports_enabled",
+ field=models.BooleanField(default=True),
+ ),
+ ]
diff --git a/bookwyrm/migrations/0167_auto_20221125_1900.py b/bookwyrm/migrations/0167_auto_20221125_1900.py
new file mode 100644
index 000000000..db258b7c5
--- /dev/null
+++ b/bookwyrm/migrations/0167_auto_20221125_1900.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.16 on 2022-11-25 19:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("bookwyrm", "0166_sitesettings_imports_enabled"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="sitesettings",
+ name="impressum",
+ field=models.TextField(default="Add a impressum here."),
+ ),
+ migrations.AddField(
+ model_name="sitesettings",
+ name="show_impressum",
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py
index 3c1494204..533a37b30 100644
--- a/bookwyrm/models/site.py
+++ b/bookwyrm/models/site.py
@@ -62,6 +62,8 @@ class SiteSettings(SiteModel):
)
code_of_conduct = models.TextField(default="Add a code of conduct here.")
privacy_policy = models.TextField(default="Add a privacy policy here.")
+ impressum = models.TextField(default="Add a impressum here.")
+ show_impressum = models.BooleanField(default=False)
# registration
allow_registration = models.BooleanField(default=False)
@@ -86,6 +88,9 @@ class SiteSettings(SiteModel):
admin_email = models.EmailField(max_length=255, null=True, blank=True)
footer_item = models.TextField(null=True, blank=True)
+ # controls
+ imports_enabled = models.BooleanField(default=True)
+
field_tracker = FieldTracker(fields=["name", "instance_tagline", "logo"])
@classmethod
diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py
index 5f7b00d87..e48d86572 100644
--- a/bookwyrm/models/user.py
+++ b/bookwyrm/models/user.py
@@ -244,9 +244,10 @@ class User(OrderedCollectionPageMixin, AbstractUser):
def admins(cls):
"""Get a queryset of the admins for this instance"""
return cls.objects.filter(
- models.Q(user_permissions__name__in=["moderate_user", "moderate_post"])
- | models.Q(is_superuser=True)
- )
+ models.Q(groups__name__in=["moderator", "admin"])
+ | models.Q(is_superuser=True),
+ is_active=True,
+ ).distinct()
def update_active_date(self):
"""this user is here! they are doing things!"""
diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py
index 0fcc00590..1a3238a1f 100644
--- a/bookwyrm/settings.py
+++ b/bookwyrm/settings.py
@@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
env = Env()
env.read_env()
DOMAIN = env("DOMAIN")
-VERSION = "0.5.1"
+VERSION = "0.5.2"
RELEASE_API = env(
"RELEASE_API",
@@ -364,3 +364,7 @@ OTEL_EXPORTER_OTLP_HEADERS = env("OTEL_EXPORTER_OTLP_HEADERS", None)
OTEL_SERVICE_NAME = env("OTEL_SERVICE_NAME", None)
TWO_FACTOR_LOGIN_MAX_SECONDS = 60
+
+HTTP_X_FORWARDED_PROTO = env.bool("SECURE_PROXY_SSL_HEADER", False)
+if HTTP_X_FORWARDED_PROTO:
+ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
diff --git a/bookwyrm/static/css/bookwyrm/_all.scss b/bookwyrm/static/css/bookwyrm/_all.scss
index 31e732ebe..1e8569827 100644
--- a/bookwyrm/static/css/bookwyrm/_all.scss
+++ b/bookwyrm/static/css/bookwyrm/_all.scss
@@ -140,6 +140,10 @@ button:focus-visible .button-invisible-overlay {
opacity: 1;
}
+button.button-paragraph {
+ vertical-align: middle;
+}
+
/** States
******************************************************************************/
diff --git a/bookwyrm/static/css/bookwyrm/components/_details.scss b/bookwyrm/static/css/bookwyrm/components/_details.scss
index de29629c8..4145554eb 100644
--- a/bookwyrm/static/css/bookwyrm/components/_details.scss
+++ b/bookwyrm/static/css/bookwyrm/components/_details.scss
@@ -81,7 +81,19 @@ details.dropdown .dropdown-menu a:focus-visible {
details.details-panel {
box-shadow: 0 0 0 1px $border;
transition: box-shadow 0.2s ease;
- padding: 0.75rem;
+ padding: 0;
+
+ > * {
+ padding: 0.75rem;
+ }
+
+ summary {
+ position: relative;
+
+ .details-close {
+ padding: 0.75rem;
+ }
+ }
}
details[open].details-panel,
@@ -89,10 +101,6 @@ 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;
diff --git a/bookwyrm/static/css/themes/bookwyrm-dark.scss b/bookwyrm/static/css/themes/bookwyrm-dark.scss
index a2eb94efb..ae904b4a4 100644
--- a/bookwyrm/static/css/themes/bookwyrm-dark.scss
+++ b/bookwyrm/static/css/themes/bookwyrm-dark.scss
@@ -15,6 +15,8 @@ $danger: #872538;
$danger-light: #481922;
$light: #393939;
$red: #ffa1b4;
+$black: #000;
+$white-ter: hsl(0, 0%, 90%);
/* book cover standins */
$no-cover-color: #002549;
@@ -56,9 +58,12 @@ $link-active: $white-bis;
$link-light: #0d1c26;
/* bulma overrides */
+$body-background-color: rgb(17, 18, 18);
$background: $background-secondary;
$menu-item-active-background-color: $link-background;
$navbar-dropdown-item-hover-color: $white;
+$info-light: $background-body;
+$info-dark: #72b6ee;
/* These element's colors are hardcoded, probably a bug in bulma? */
@media screen and (min-width: 769px) {
@@ -74,7 +79,7 @@ $navbar-dropdown-item-hover-color: $white;
}
/* misc */
-$shadow: 0 0.5em 1em -0.125em rgba($black, 0.2), 0 0px 0 1px rgba($black, 0.02);
+$shadow: 0 0.5em 0.5em -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;
@@ -92,6 +97,11 @@ $family-secondary: $family-sans-serif;
color: $grey-light !important;
}
+
+#qrcode svg {
+ background-color: #a6a6a6;
+}
+
@import "../bookwyrm.scss";
@import "../vendor/icons.css";
@import "../vendor/shepherd.scss";
diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js
index aa06a8b0a..5b3f13d4a 100644
--- a/bookwyrm/static/js/bookwyrm.js
+++ b/bookwyrm/static/js/bookwyrm.js
@@ -628,9 +628,9 @@ let BookWyrm = new (class {
}
function toggleStatus(status) {
- for (const child of statusNode.children) {
- BookWyrm.toggleContainer(child, !child.classList.contains(status));
- }
+ const template = document.querySelector(`#barcode-${status}`);
+
+ statusNode.replaceChildren(template ? template.content.cloneNode(true) : null);
}
function initBarcodes(cameraId = null) {
diff --git a/bookwyrm/templates/about/about.html b/bookwyrm/templates/about/about.html
index 481ecda99..c446e0cf2 100644
--- a/bookwyrm/templates/about/about.html
+++ b/bookwyrm/templates/about/about.html
@@ -11,7 +11,7 @@
{% block about_content %}
{# seven day cache #}
-{% cache 604800 about_page %}
+{% cache 604800 about_page_superlatives %}
{% get_book_superlatives as superlatives %}