Merge branch 'main' into user-migration

This commit is contained in:
Hugh Rundle 2023-10-22 18:47:41 +11:00 committed by GitHub
commit 8477d0b89d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
91 changed files with 6697 additions and 3892 deletions

1
.prettierrc Normal file
View file

@ -0,0 +1 @@
'trailingComma': 'es5'

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.7.0

View file

@ -4,7 +4,11 @@ import sys
from .base_activity import ActivityEncoder, Signature, naive_parse
from .base_activity import Link, Mention, Hashtag
from .base_activity import ActivitySerializerError, resolve_remote_id
from .base_activity import (
ActivitySerializerError,
resolve_remote_id,
get_representative,
)
from .image import Document, Image
from .note import Note, GeneratedNote, Article, Comment, Quotation
from .note import Review, Rating

View file

@ -201,14 +201,13 @@ class Book(BookDataModel):
@property
def alt_text(self):
"""image alt test"""
text = self.title
if self.edition_info:
text += f" ({self.edition_info})"
return text
author = f"{name}: " if (name := self.author_text) else ""
edition = f" ({info})" if (info := self.edition_info) else ""
return f"{author}{self.title}{edition}"
def save(self, *args: Any, **kwargs: Any) -> None:
"""can't be abstract for query reasons, but you shouldn't USE it"""
if not isinstance(self, Edition) and not isinstance(self, Work):
if not isinstance(self, (Edition, Work)):
raise ValueError("Books should be added as Editions or Works")
return super().save(*args, **kwargs)

View file

@ -1,5 +1,6 @@
""" activitypub-aware django model fields """
from dataclasses import MISSING
from datetime import datetime
import re
from uuid import uuid4
from urllib.parse import urljoin
@ -534,8 +535,10 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
return value.isoformat()
def field_from_activity(self, value, allow_external_connections=True):
missing_fields = datetime(1970, 1, 1) # "2022-10" => "2022-10-01"
try:
date_value = dateutil.parser.parse(value)
# TODO(dato): investigate `ignoretz=True` wrt bookwyrm#3028.
date_value = dateutil.parser.parse(value, default=missing_fields)
try:
return timezone.make_aware(date_value)
except ValueError:

View file

@ -1,4 +1,5 @@
""" track progress of goodreads imports """
from datetime import datetime
import math
import re
import dateutil.parser
@ -259,38 +260,30 @@ class ImportItem(models.Model):
except ValueError:
return None
def _parse_datefield(self, field, /):
if not (date := self.normalized_data.get(field)):
return None
defaults = datetime(1970, 1, 1) # "2022-10" => "2022-10-01"
parsed = dateutil.parser.parse(date, default=defaults)
# Keep timezone if import already had one, else use default.
return parsed if timezone.is_aware(parsed) else timezone.make_aware(parsed)
@property
def date_added(self):
"""when the book was added to this dataset"""
if self.normalized_data.get("date_added"):
parsed_date_added = dateutil.parser.parse(
self.normalized_data.get("date_added")
)
if timezone.is_aware(parsed_date_added):
# Keep timezone if import already had one
return parsed_date_added
return timezone.make_aware(parsed_date_added)
return None
return self._parse_datefield("date_added")
@property
def date_started(self):
"""when the book was started"""
if self.normalized_data.get("date_started"):
return timezone.make_aware(
dateutil.parser.parse(self.normalized_data.get("date_started"))
)
return None
return self._parse_datefield("date_started")
@property
def date_read(self):
"""the date a book was completed"""
if self.normalized_data.get("date_finished"):
return timezone.make_aware(
dateutil.parser.parse(self.normalized_data.get("date_finished"))
)
return None
return self._parse_datefield("date_finished")
@property
def reads(self):

View file

@ -366,7 +366,8 @@ class Quotation(BookStatus):
quote = re.sub(r"^<p>", '<p>"', self.quote)
quote = re.sub(r"</p>$", '"</p>', quote)
title, href = self.book.title, self.book.remote_id
citation = f'— <a href="{href}"><i>{title}</i></a>'
author = f"{name}: " if (name := self.book.author_text) else ""
citation = f'{author}<a href="{href}"><i>{title}</i></a>'
if position := self._format_position():
citation += f", {position}"
return f"{quote} <p>{citation}</p>{self.content}"

View file

@ -4,6 +4,7 @@ from typing import AnyStr
from environs import Env
import requests
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ImproperlyConfigured
@ -14,7 +15,13 @@ from django.core.exceptions import ImproperlyConfigured
env = Env()
env.read_env()
DOMAIN = env("DOMAIN")
VERSION = "0.6.5"
with open("VERSION", encoding="utf-8") as f:
version = f.read()
version = version.replace("\n", "")
f.close()
VERSION = version
RELEASE_API = env(
"RELEASE_API",
@ -24,7 +31,7 @@ RELEASE_API = env(
PAGE_LENGTH = env.int("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
JS_CACHE = "b972a43c"
JS_CACHE = "ac315a3b"
# email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
@ -317,6 +324,7 @@ LANGUAGES = [
LANGUAGE_ARTICLES = {
"English": {"the", "a", "an"},
"Español (Spanish)": {"un", "una", "unos", "unas", "el", "la", "los", "las"},
}
TIME_ZONE = "UTC"

View file

@ -1,4 +1,3 @@
@charset "utf-8";
@import "vendor/bulma/bulma.sass";
@import "bookwyrm/all.scss";
@import "vendor/bulma/bulma";
@import "bookwyrm/all";

View file

@ -16,9 +16,7 @@
@import "components/status";
@import "components/tabs";
@import "components/toggle";
@import "overrides/bulma_overrides";
@import "utilities/a11y";
@import "utilities/alignments";
@import "utilities/colors";
@ -40,10 +38,12 @@ body {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-thumb {
background: $scrollbar-thumb;
border-radius: 0.5em;
}
::-webkit-scrollbar-track {
background: $scrollbar-track;
}
@ -89,7 +89,6 @@ button::-moz-focus-inner {
/** Utilities not covered by Bulma
******************************************************************************/
.tag.is-small {
height: auto;
}
@ -144,7 +143,6 @@ button.button-paragraph {
vertical-align: middle;
}
/** States
******************************************************************************/
@ -159,7 +157,6 @@ button.button-paragraph {
cursor: not-allowed;
}
/* Notifications page
******************************************************************************/

View file

@ -43,7 +43,7 @@
max-height: 100%;
/* Useful when stretching under-sized images. */
image-rendering: optimizeQuality;
image-rendering: optimizequality;
image-rendering: smooth;
}

View file

@ -30,20 +30,20 @@
}
.copy-tooltip {
overflow: visible;
visibility: hidden;
width: 140px;
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
margin-left: -30px;
margin-top: -45px;
opacity: 0;
transition: opacity 0.3s;
overflow: visible;
visibility: hidden;
width: 140px;
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
margin-left: -30px;
margin-top: -45px;
opacity: 0;
transition: opacity 0.3s;
}
.copy-tooltip::after {
@ -54,5 +54,5 @@
margin-left: -60px;
border-width: 5px;
border-style: solid;
border-color: #555 transparent transparent transparent;
}
border-color: #555 transparent transparent;
}

View file

@ -44,12 +44,12 @@
.bw-tabs a:hover {
border-bottom-color: transparent;
color: $text
color: $text;
}
.bw-tabs a.is-active {
border-bottom-color: transparent;
color: $link
color: $link;
}
.bw-tabs.is-left {

View file

@ -43,6 +43,8 @@
<glyph unicode="&#xe937;" 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="&#xe97a;" 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="&#xe986;" 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="&#xe9ce;" glyph-name="eye" d="M512 768c-223.318 0-416.882-130.042-512-320 95.118-189.958 288.682-320 512-320 223.312 0 416.876 130.042 512 320-95.116 189.958-288.688 320-512 320zM764.45 598.296c60.162-38.374 111.142-89.774 149.434-150.296-38.292-60.522-89.274-111.922-149.436-150.296-75.594-48.218-162.89-73.704-252.448-73.704-89.56 0-176.858 25.486-252.452 73.704-60.158 38.372-111.138 89.772-149.432 150.296 38.292 60.524 89.274 111.924 149.434 150.296 3.918 2.5 7.876 4.922 11.86 7.3-9.96-27.328-15.41-56.822-15.41-87.596 0-141.382 114.616-256 256-256 141.382 0 256 114.618 256 256 0 30.774-5.452 60.268-15.408 87.598 3.978-2.378 7.938-4.802 11.858-7.302v0zM512 544c0-53.020-42.98-96-96-96s-96 42.98-96 96 42.98 96 96 96 96-42.982 96-96z" />
<glyph unicode="&#xe9d1;" glyph-name="eye-blocked" d="M945.942 945.942c-18.746 18.744-49.136 18.744-67.882 0l-202.164-202.164c-51.938 15.754-106.948 24.222-163.896 24.222-223.318 0-416.882-130.042-512-320 41.122-82.124 100.648-153.040 173.022-207.096l-158.962-158.962c-18.746-18.746-18.746-49.136 0-67.882 9.372-9.374 21.656-14.060 33.94-14.060s24.568 4.686 33.942 14.058l864 864c18.744 18.746 18.744 49.138 0 67.884zM416 640c42.24 0 78.082-27.294 90.92-65.196l-121.724-121.724c-37.902 12.838-65.196 48.68-65.196 90.92 0 53.020 42.98 96 96 96zM110.116 448c38.292 60.524 89.274 111.924 149.434 150.296 3.918 2.5 7.876 4.922 11.862 7.3-9.962-27.328-15.412-56.822-15.412-87.596 0-54.89 17.286-105.738 46.7-147.418l-60.924-60.924c-52.446 36.842-97.202 83.882-131.66 138.342zM768 518c0 27.166-4.256 53.334-12.102 77.898l-321.808-321.808c24.568-7.842 50.742-12.090 77.91-12.090 141.382 0 256 114.618 256 256zM830.026 670.026l-69.362-69.362c1.264-0.786 2.53-1.568 3.786-2.368 60.162-38.374 111.142-89.774 149.434-150.296-38.292-60.522-89.274-111.922-149.436-150.296-75.594-48.218-162.89-73.704-252.448-73.704-38.664 0-76.902 4.76-113.962 14.040l-76.894-76.894c59.718-21.462 123.95-33.146 190.856-33.146 223.31 0 416.876 130.042 512 320-45.022 89.916-112.118 166.396-193.974 222.026z" />
<glyph unicode="&#xe9d7;" 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" />
<glyph unicode="&#xe9d8;" glyph-name="star-half" 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-0.942-0.496 0.942 570.768 111.736-226.396 249.836-36.304-180.788-176.222 42.678-248.83-223.462 117.48z" />
<glyph unicode="&#xe9d9;" glyph-name="star-full" 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.538z" />

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -1,4 +1,4 @@
@import "../vendor/bulma/sass/utilities/initial-variables.sass";
@import "../vendor/bulma/sass/utilities/initial-variables";
/* Colors
******************************************************************************/
@ -16,7 +16,7 @@ $danger-light: #481922;
$light: #393939;
$red: #ffa1b4;
$black: #000;
$white-ter: hsl(0, 0%, 90%);
$white-ter: hsl(0deg, 0%, 90%);
/* book cover standins */
$no-cover-color: #002549;
@ -79,7 +79,7 @@ $info-dark: #72b6ee;
}
/* misc */
$shadow: 0 0.5em 0.5em -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 0 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;
@ -97,27 +97,23 @@ $family-secondary: $family-sans-serif;
color: $grey-light !important;
}
.tabs li:not(.is-active) a {
color: #2e7eb9 !important;
}
.tabs li:not(.is-active) a:hover {
.tabs li:not(.is-active) a:hover {
border-bottom-color: #2e7eb9 !important;
}
.tabs li:not(.is-active) a {
color: #2e7eb9 !important;
}
.tabs li.is-active a {
color: #e6e6e6 !important;
border-bottom-color: #e6e6e6 !important ;
border-bottom-color: #e6e6e6 !important;
}
#qrcode svg {
background-color: #a6a6a6;
}
@import "../bookwyrm.scss";
@import "../bookwyrm";
@import "../vendor/icons.css";
@import "../vendor/shepherd.scss";
@import "../vendor/shepherd";

View file

@ -1,4 +1,4 @@
@import "../vendor/bulma/sass/utilities/derived-variables.sass";
@import "../vendor/bulma/sass/utilities/derived-variables";
/* Colors
******************************************************************************/
@ -68,19 +68,16 @@ $family-secondary: $family-sans-serif;
.tabs li:not(.is-active) a {
color: #3273dc !important;
}
.tabs li:not(.is-active) a:hover {
border-bottom-color: #3273dc !important;
}
.tabs li:not(.is-active) a {
color: #3273dc !important;
.tabs li:not(.is-active) a:hover {
border-bottom-color: #3273dc !important;
}
.tabs li.is-active a {
color: #4a4a4a !important;
border-bottom-color: #4a4a4a !important ;
border-bottom-color: #4a4a4a !important;
}
@import "../bookwyrm.scss";
@import "../bookwyrm";
@import "../vendor/icons.css";
@import "../vendor/shepherd.scss";
@import "../vendor/shepherd";

View file

@ -155,3 +155,9 @@
.icon-barcode:before {
content: "\e937";
}
.icon-eye:before {
content: "\e9ce";
}
.icon-eye-blocked:before {
content: "\e9d1";
}

View file

@ -30,6 +30,12 @@ let BookWyrm = new (class {
.querySelectorAll("[data-back]")
.forEach((button) => button.addEventListener("click", this.back));
document
.querySelectorAll("[data-password-icon]")
.forEach((button) =>
button.addEventListener("click", this.togglePasswordVisibility.bind(this))
);
document
.querySelectorAll('input[type="file"]')
.forEach((node) => node.addEventListener("change", this.disableIfTooLarge.bind(this)));
@ -820,4 +826,24 @@ let BookWyrm = new (class {
form.querySelector('input[name="preferred_timezone"]').value = tz;
}
togglePasswordVisibility(event) {
const iconElement = event.currentTarget.getElementsByTagName("button")[0];
const passwordElementId = event.currentTarget.dataset.for;
const passwordInputElement = document.getElementById(passwordElementId);
if (!passwordInputElement) return;
if (passwordInputElement.type === "password") {
passwordInputElement.type = "text";
this.addRemoveClass(iconElement, "icon-eye-blocked");
this.addRemoveClass(iconElement, "icon-eye", true);
} else {
passwordInputElement.type = "password";
this.addRemoveClass(iconElement, "icon-eye");
this.addRemoveClass(iconElement, "icon-eye-blocked", true);
}
this.toggleFocus(passwordElementId);
}
})();

View file

@ -47,12 +47,11 @@
.querySelectorAll("[data-remove]")
.forEach((node) => node.addEventListener("click", removeInput));
// Get the element, add a keypress listener...
// Get element, add a keypress listener...
document.getElementById("subjects").addEventListener("keypress", function (e) {
// e.target is the element where it listens!
// if e.target is input field within the "subjects" div, do stuff
// Linstening to element e.target
// If e.target is an input field within "subjects" div preventDefault()
if (e.target && e.target.nodeName == "INPUT") {
// Item found, prevent default
if (event.keyCode == 13) {
event.preventDefault();
}

View file

@ -144,14 +144,6 @@
</a>
</div>
{% endif %}
{% if author.isfdb %}
<div>
<a itemprop="sameAs" href="https://www.isfdb.org/cgi-bin/ea.cgi?{{ author.isfdb }}" target="_blank" rel="nofollow noopener noreferrer">
{% trans "View ISFDB entry" %}
</a>
</div>
{% endif %}
</div>
</section>
{% endif %}

View file

@ -44,16 +44,18 @@
{% endif %}
{% if book.series %}
<meta itemprop="isPartOf" content="{{ book.series | escape }}">
<meta itemprop="volumeNumber" content="{{ book.series_number }}">
<meta itemprop="position" content="{{ book.series_number }}">
<span itemprop="isPartOf" itemscope itemtype="https://schema.org/BookSeries">
{% if book.authors.exists %}
<a href="{% url 'book-series-by' book.authors.first.id %}?series_name={{ book.series }}">
<a href="{% url 'book-series-by' book.authors.first.id %}?series_name={{ book.series | urlencode }}"
itemprop="url">
{% endif %}
{{ book.series }}{% if book.series_number %} #{{ book.series_number }}{% endif %}
<span itemprop="name">{{ book.series }}</span>
{% if book.series_number %} #{{ book.series_number }}{% endif %}
{% if book.authors.exists %}
</a>
{% endif %}
</span>
{% endif %}
</p>
{% endif %}
@ -186,8 +188,6 @@
itemtype="https://schema.org/AggregateRating"
>
<meta itemprop="ratingValue" content="{{ rating|floatformat }}">
{# @todo Is it possible to not hard-code the value? #}
<meta itemprop="bestRating" content="5">
<meta itemprop="reviewCount" content="{{ review_count }}">
<span>

View file

@ -40,16 +40,13 @@
</p>
{% endif %}
{% with date=book.published_date|naturalday publisher=book.publishers|join:', ' %}
{% if date or book.first_published_date or book.publishers %}
{% if date or book.first_published_date %}
{% if book.published_date or book.first_published_date %}
<meta
itemprop="datePublished"
content="{{ book.first_published_date|default:book.published_date|date:'Y-m-d' }}"
>
{% endif %}
<p>
{% comment %}
@todo The publisher property needs to be an Organization or a Person. Well be using Thing which is the more generic ancestor.
@see https://schema.org/Publisher
@ -60,14 +57,14 @@
{% endfor %}
{% endif %}
{% if date and publisher %}
{% with date=book.published_date|default:book.first_published_date|naturalday publisher=book.publishers|join:', ' %}
{% if book.published_date and publisher %}
{% blocktrans %}Published {{ date }} by {{ publisher }}.{% endblocktrans %}
{% elif date %}
{% blocktrans %}Published {{ date }}{% endblocktrans %}
{% elif publisher %}
{% blocktrans %}Published by {{ publisher }}.{% endblocktrans %}
{% elif date %}
{% blocktrans %}Published {{ date }}{% endblocktrans %}
{% endif %}
{% endwith %}
</p>
{% endif %}
{% endwith %}
{% endspaceless %}

View file

@ -5,13 +5,18 @@
{% include 'snippets/avatar.html' with user=user %}
</div>
<div class="media-content">
<div>
<a href="{{ user.local_path }}">{{ user.display_name }}</a>
<div class="media-content" itemprop="review" itemscope itemtype="https://schema.org/Review">
<div itemprop="author"
itemscope
itemtype="https://schema.org/Person"
>
<a href="{{ user.local_path }}" itemprop="url">
<span itemprop="name">{{ user.display_name }}</span>
</a>
</div>
<div class="is-flex">
<div class="is-flex" itemprop="reviewRating" itemscope itemtype="https://schema.org/Rating">
<meta itemprop="ratingValue" content="{{ rating.rating|floatformat }}">
<p class="mr-1">{% trans "rated it" %}</p>
{% include 'snippets/stars.html' with rating=rating.rating %}
</div>
<div>

View file

@ -5,15 +5,15 @@
{% block title %}{{ series_name }}{% endblock %}
{% block content %}
<div class="block">
<h1 class="title">{{ series_name }}</h1>
<div class="block" itemscope itemtype="https://schema.org/BookSeries">
<h1 class="title" itemprop="name">{{ series_name }}</h1>
<div class="subtitle" dir="auto">
{% trans "Series by" %} <a
href="{{ author.local_path }}"
class="author {{ link_class }}"
itemprop="author"
itemprop="creator"
itemscope
itemtype="https://schema.org/Thing"
itemtype="https://schema.org/Person"
><span
itemprop="name"
>{{ author.name }}</span></a>
@ -22,6 +22,7 @@
<div class="columns is-multiline is-mobile">
{% for book in books %}
{% with book=book %}
{# @todo Set `hasPart` property in some meaningful way #}
<div class="column is-one-fifth-tablet is-half-mobile is-flex is-flex-direction-column">
<div class="is-flex-grow-1 mb-3">
<span class="subtitle">{% if book.series_number %}{% blocktrans with series_number=book.series_number %}Book {{ series_number }}{% endblocktrans %}{% else %}{% trans 'Unsorted Book' %}{% endif %}</span>

View file

@ -12,6 +12,7 @@
<base target="_blank">
<link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{% get_media_prefix %}{{ site.favicon }}{% else %}{% static "images/favicon.ico" %}{% endif %}">
<link rel="manifest" href="/manifest.json" />
</head>
<body>

View file

@ -18,7 +18,7 @@
{% if import_size_limit and import_limit_reset %}
<div class="notification">
<p>
{% blocktrans count days=import_limit_reset with display_size=import_size_limit|intcomma %}
{% blocktrans trimmed count days=import_limit_reset with display_size=import_size_limit|intcomma %}
Currently, you are allowed to import {{ display_size }} books every {{ import_limit_reset }} day.
{% plural %}
Currently, you are allowed to import {{ import_size_limit }} books every {{ import_limit_reset }} days.

View file

@ -14,6 +14,7 @@
<link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{% get_media_prefix %}{{ site.favicon }}{% else %}{% static "images/favicon.ico" %}{% endif %}">
<link rel="apple-touch-icon" href="{% if site.logo %}{{ media_full_url }}{{ site.logo }}{% else %}{% static "images/logo.png" %}{% endif %}">
<link rel="manifest" href="/manifest.json" />
{% block opengraph %}
{% include 'snippets/opengraph.html' %}
@ -129,7 +130,12 @@
</div>
<div class="column">
<label class="is-sr-only" for="id_password">{% trans "Password:" %}</label>
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password" placeholder="{% trans 'password' %}">
<div class="control has-icons-right">
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password" placeholder="{% trans 'password' %}">
<span data-password-icon data-for="id_password" class="icon is-right is-clickable">
<button type="button" aria-controls="id_password" class="icon-eye-blocked" title="{% trans 'Show/Hide password' %}"></button>
</span>
</div>
<p class="help"><a href="{% url 'password-reset' %}">{% trans "Forgot your password?" %}</a></p>
</div>
<div class="column is-narrow">

View file

@ -0,0 +1,14 @@
{% load static %}
{
"name": "{{ site.name }}",
"description": "{{ site.description }}",
"icons": [
{
"src": "{% if site.logo %}{{ media_full_url }}{{ site.logo }}{% else %}{% static 'images/logo.png' %}{% endif %}",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": "/",
"display": "standalone"
}

View file

@ -75,13 +75,13 @@
{% include 'snippets/form_errors.html' with errors_list=form.invite_request_text.errors id="desc_invite_request_text" %}
</div>
<div class="field">
<label class="label" for="id_invite_requests_question">
<label class="label">
{{ form.invite_request_question }}
{% trans "Set a question for invite requests" %}
</label>
</div>
<div class="field">
<label class="label" for="id_invite_question_text">
<label class="label">
{% trans "Question:" %}
{{ form.invite_question_text }}
</label>

View file

@ -45,7 +45,7 @@
{% include 'snippets/form_errors.html' with errors_list=form.invite_request_text.errors id="desc_invite_request_text" %}
</div>
<div class="field">
<label class="label" for="id_invite_requests_question">
<label class="label">
{{ form.invite_request_question }}
{% trans "Set a question for invite requests" %}
</label>

View file

@ -6,14 +6,6 @@
{% load humanize %}
{% with status_type=status.status_type %}
<div
class="block"
{% if status_type == "Review" %}
itemprop="rating"
itemtype="https://schema.org/Rating"
{% endif %}
>
<div class="columns is-gapless">
{% if not hide_book %}
{% with book=status.book|default:status.mention_books.first %}
@ -58,9 +50,6 @@
{% endif %}
>
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
{# @todo Is it possible to not hard-code the value? #}
<meta itemprop="bestRating" content="5">
</span>
{% include 'snippets/stars.html' with rating=status.rating %}
</h4>
@ -154,6 +143,5 @@
</div>
</article>
</div>
</div>
{% endwith %}

View file

@ -16,9 +16,6 @@
>
<span class="is-hidden" {{ rating_type }}>
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
{# @todo Is it possible to not hard-code the value? #}
<meta itemprop="bestRating" content="5">
</span>
</span>
</span>

View file

@ -92,7 +92,23 @@ class Book(TestCase):
book.published_date = timezone.make_aware(parse("2020"))
book.save()
self.assertEqual(book.edition_info, "worm, Glorbish language, 2020")
self.assertEqual(book.alt_text, "Test Edition (worm, Glorbish language, 2020)")
def test_alt_text(self):
"""text slug used for cover images"""
book = models.Edition.objects.create(title="Test Edition")
author = models.Author.objects.create(name="Author Name")
self.assertEqual(book.alt_text, "Test Edition")
book.authors.set([author])
book.save()
self.assertEqual(book.alt_text, "Author Name: Test Edition")
book.physical_format = "worm"
book.published_date = timezone.make_aware(parse("2022"))
self.assertEqual(book.alt_text, "Author Name: Test Edition (worm, 2022)")
def test_get_rank(self):
"""sets the data quality index for the book"""

View file

@ -314,6 +314,29 @@ class Status(TestCase):
)
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
def test_quotation_with_author_to_pure_activity(self, *_):
"""serialization of quotation of a book with author and edition info"""
self.book.authors.set([models.Author.objects.create(name="Author Name")])
self.book.physical_format = "worm"
self.book.save()
status = models.Quotation.objects.create(
quote="quote",
content="",
user=self.local_user,
book=self.book,
)
activity = status.to_activity(pure=True)
self.assertEqual(
activity["content"],
(
f'quote <p>— Author Name: <a href="{self.book.remote_id}">'
"<i>Test Edition</i></a></p>"
),
)
self.assertEqual(
activity["attachment"][0]["name"], "Author Name: Test Edition (worm)"
)
def test_quotation_page_serialization(self, *_):
"""serialization of quotation page position"""
tests = [

View file

@ -7,6 +7,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import timezone
from bookwyrm import forms, models, views
from bookwyrm.tests.validate_html import validate_html
@ -128,7 +129,7 @@ class ImportViews(TestCase):
def test_get_average_import_time_with_data(self):
"""Now, with data"""
now = datetime.datetime.now()
now = timezone.now()
two_hours_ago = now - datetime.timedelta(hours=2)
four_hours_ago = now - datetime.timedelta(hours=4)
models.ImportJob.objects.create(
@ -152,7 +153,7 @@ class ImportViews(TestCase):
def test_get_average_import_time_ignore_stopped(self):
"""Don't include stopped, do include no status"""
now = datetime.datetime.now()
now = timezone.now()
two_hours_ago = now - datetime.timedelta(hours=2)
four_hours_ago = now - datetime.timedelta(hours=4)
models.ImportJob.objects.create(

View file

@ -156,7 +156,7 @@ class Views(TestCase):
response = view(request)
validate_html(response.render())
self.assertFalse("results" in response.context_data)
self.assertTrue("results" in response.context_data)
def test_search_lists(self):
"""searches remote connectors"""

View file

@ -72,7 +72,7 @@ class SetupViews(TestCase):
self.site.refresh_from_db()
self.assertFalse(self.site.install_mode)
user = models.User.objects.get()
user = models.User.objects.first()
self.assertTrue(user.is_active)
self.assertTrue(user.is_superuser)
self.assertTrue(user.is_staff)

View file

@ -34,6 +34,12 @@ urlpatterns = [
"robots.txt",
TemplateView.as_view(template_name="robots.txt", content_type="text/plain"),
),
path(
"manifest.json",
TemplateView.as_view(
template_name="manifest.json", content_type="application/json"
),
),
# federation endpoints
re_path(r"^inbox/?$", views.Inbox.as_view(), name="inbox"),
re_path(rf"{LOCAL_USER_PATH}/inbox/?$", views.Inbox.as_view(), name="user_inbox"),

View file

@ -91,18 +91,15 @@ def book_search(request):
def user_search(request):
"""cool kids members only user search"""
"""user search: search for a user"""
viewer = request.user
query = request.GET.get("q")
query = query.strip()
data = {"type": "user", "query": query}
# logged out viewers can't search users
if not viewer.is_authenticated:
return TemplateResponse(request, "search/user.html", data)
# use webfinger for mastodon style account@domain.com username to load the user if
# they don't exist locally (handle_remote_webfinger will check the db)
if re.match(regex.FULL_USERNAME, query):
if re.match(regex.FULL_USERNAME, query) and viewer.is_authenticated:
handle_remote_webfinger(query)
results = (
@ -118,6 +115,11 @@ def user_search(request):
)
.order_by("-similarity")
)
# don't expose remote users
if not viewer.is_authenticated:
results = results.filter(local=True)
paginated = Paginator(results, PAGE_LENGTH)
page = paginated.get_page(request.GET.get("page"))
data["results"] = page

View file

@ -9,6 +9,7 @@ from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.views import View
from bookwyrm.activitypub import get_representative
from bookwyrm import forms, models
from bookwyrm import settings
from bookwyrm.utils import regex
@ -96,4 +97,5 @@ class CreateAdmin(View):
login(request, user)
site.install_mode = False
site.save()
get_representative() # create the instance user
return redirect("settings-site")

80
bump-version.sh Executable file
View file

@ -0,0 +1,80 @@
#!/bin/bash
NOW="$(date +'%B %d, %Y')"
RED="\033[1;31m"
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
BLUE="\033[1;34m"
PURPLE="\033[1;35m"
CYAN="\033[1;36m"
WHITE="\033[1;37m"
RESET="\033[0m"
LATEST_HASH=`git log --pretty=format:'%h' -n 1`
QUESTION_FLAG="${GREEN}?"
WARNING_FLAG="${YELLOW}!"
NOTICE_FLAG="${CYAN}"
# ADJUSTMENTS_MSG="${QUESTION_FLAG} ${CYAN}Now you can make adjustments to ${WHITE}CHANGELOG.md${CYAN}. Then press enter to continue."
PUSHING_MSG="${NOTICE_FLAG} Pushing new version to the ${WHITE}origin${CYAN}..."
if [ -f VERSION ]; then
BASE_STRING=`cat VERSION`
BASE_LIST=(`echo $BASE_STRING | tr '.' ' '`)
V_MAJOR=${BASE_LIST[0]}
V_MINOR=${BASE_LIST[1]}
V_PATCH=${BASE_LIST[2]}
echo -e "${NOTICE_FLAG} Current version: ${WHITE}$BASE_STRING"
echo -e "${NOTICE_FLAG} Latest commit hash: ${WHITE}$LATEST_HASH"
V_MINOR=$((V_MINOR + 1))
V_PATCH=0
SUGGESTED_VERSION="$V_MAJOR.$V_MINOR.$V_PATCH"
echo -ne "${QUESTION_FLAG} ${CYAN}Enter a version number [${WHITE}$SUGGESTED_VERSION${CYAN}]: "
read INPUT_STRING
if [ "$INPUT_STRING" = "" ]; then
INPUT_STRING=$SUGGESTED_VERSION
fi
echo -e "${NOTICE_FLAG} Will set new version to be ${WHITE}$INPUT_STRING"
echo $INPUT_STRING > VERSION
# echo "## $INPUT_STRING ($NOW)" > tmpfile
# git log --pretty=format:" - %s" "v$BASE_STRING"...HEAD >> tmpfile
# echo "" >> tmpfile
# echo "" >> tmpfile
# cat CHANGELOG.md >> tmpfile
# mv tmpfile CHANGELOG.md
# echo -e "$ADJUSTMENTS_MSG"
# read
echo -e "$PUSHING_MSG"
# git add CHANGELOG.md
git add VERSION
git commit -m "Bump version to ${INPUT_STRING}."
git tag -a -m "Tag version ${INPUT_STRING}." "v$INPUT_STRING"
git push origin --tags
else
echo -e "${WARNING_FLAG} Could not find a VERSION file."
echo -ne "${QUESTION_FLAG} ${CYAN}Do you want to create a version file and start from scratch? [${WHITE}y${CYAN}]: "
read RESPONSE
if [ "$RESPONSE" = "" ]; then RESPONSE="y"; fi
if [ "$RESPONSE" = "Y" ]; then RESPONSE="y"; fi
if [ "$RESPONSE" = "Yes" ]; then RESPONSE="y"; fi
if [ "$RESPONSE" = "yes" ]; then RESPONSE="y"; fi
if [ "$RESPONSE" = "YES" ]; then RESPONSE="y"; fi
if [ "$RESPONSE" = "y" ]; then
echo "0.1.0" > VERSION
# echo "## 0.1.0 ($NOW)" > CHANGELOG.md
# git log --pretty=format:" - %s" >> CHANGELOG.md
# echo "" >> CHANGELOG.md
# echo "" >> CHANGELOG.md
# echo -e "$ADJUSTMENTS_MSG"
# read
echo -e "$PUSHING_MSG"
# git add CHANGELOG.md
git add VERSION
git commit -m "Add VERSION and CHANGELOG.md files, Bump version to v0.1.0."
git tag -a -m "Tag version 0.1.0." "v0.1.0"
git push origin --tags
fi
fi
echo -e "${NOTICE_FLAG} Finished."

18
bw-dev
View file

@ -188,27 +188,25 @@ case "$CMD" in
;;
prettier)
prod_error
$DOCKER_COMPOSE run --rm dev-tools npx prettier --write bookwyrm/static/js/*.js
$DOCKER_COMPOSE run --rm dev-tools prettier --write bookwyrm/static/js/*.js
;;
eslint)
prod_error
$DOCKER_COMPOSE run --rm dev-tools npx eslint bookwyrm/static --ext .js
$DOCKER_COMPOSE run --rm dev-tools eslint bookwyrm/static --ext .js
;;
stylelint)
prod_error
$DOCKER_COMPOSE run --rm dev-tools npx stylelint \
bookwyrm/static/css/bookwyrm.scss bookwyrm/static/css/bookwyrm/**/*.scss --fix \
--config dev-tools/.stylelintrc.js
$DOCKER_COMPOSE run --rm dev-tools stylelint --fix bookwyrm/static/css \
--config dev-tools/.stylelintrc.js --ignore-path dev-tools/.stylelintignore
;;
formatters)
prod_error
runweb pylint bookwyrm/
$DOCKER_COMPOSE run --rm dev-tools black celerywyrm bookwyrm
$DOCKER_COMPOSE run --rm dev-tools npx prettier --write bookwyrm/static/js/*.js
$DOCKER_COMPOSE run --rm dev-tools npx eslint bookwyrm/static --ext .js
$DOCKER_COMPOSE run --rm dev-tools npx stylelint \
bookwyrm/static/css/bookwyrm.scss bookwyrm/static/css/bookwyrm/**/*.scss --fix \
--config dev-tools/.stylelintrc.js
$DOCKER_COMPOSE run --rm dev-tools prettier --write bookwyrm/static/js/*.js
$DOCKER_COMPOSE run --rm dev-tools eslint bookwyrm/static --ext .js
$DOCKER_COMPOSE run --rm dev-tools stylelint --fix bookwyrm/static/css \
--config dev-tools/.stylelintrc.js --ignore-path dev-tools/.stylelintignore
;;
mypy)
prod_error

View file

@ -5,10 +5,30 @@ After=network.target postgresql.service redis.service
[Service]
User=bookwyrm
Group=bookwyrm
WorkingDirectory=/opt/bookwyrm/
WorkingDirectory=/opt/bookwyrm
ExecStart=/opt/bookwyrm/venv/bin/celery -A celerywyrm beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler
StandardOutput=journal
StandardError=inherit
ProtectSystem=strict
ProtectHome=tmpfs
InaccessiblePaths=-/media -/mnt -/srv
PrivateTmp=yes
TemporaryFileSystem=/var /run /opt
PrivateUsers=true
PrivateDevices=true
BindReadOnlyPaths=/opt/bookwyrm
BindPaths=/opt/bookwyrm/images /opt/bookwyrm/static /var/run/postgresql
LockPersonality=yes
MemoryDenyWriteExecute=true
PrivateMounts=true
ProtectHostname=true
ProtectClock=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
RestrictRealtime=true
RestrictNamespaces=net
[Install]
WantedBy=multi-user.target

View file

@ -5,10 +5,30 @@ After=network.target postgresql.service redis.service
[Service]
User=bookwyrm
Group=bookwyrm
WorkingDirectory=/opt/bookwyrm/
WorkingDirectory=/opt/bookwyrm
ExecStart=/opt/bookwyrm/venv/bin/celery -A celerywyrm worker -l info -Q high_priority,medium_priority,low_priority,streams,images,suggested_users,email,connectors,lists,inbox,imports,import_triggered,broadcast,misc
StandardOutput=journal
StandardError=inherit
ProtectSystem=strict
ProtectHome=tmpfs
InaccessiblePaths=-/media -/mnt -/srv
PrivateTmp=yes
TemporaryFileSystem=/var /run /opt
PrivateUsers=true
PrivateDevices=true
BindReadOnlyPaths=/opt/bookwyrm
BindPaths=/opt/bookwyrm/images /opt/bookwyrm/static /var/run/postgresql
LockPersonality=yes
MemoryDenyWriteExecute=true
PrivateMounts=true
ProtectHostname=true
ProtectClock=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
RestrictRealtime=true
RestrictNamespaces=net
[Install]
WantedBy=multi-user.target

View file

@ -5,10 +5,30 @@ After=network.target postgresql.service redis.service
[Service]
User=bookwyrm
Group=bookwyrm
WorkingDirectory=/opt/bookwyrm/
WorkingDirectory=/opt/bookwyrm
ExecStart=/opt/bookwyrm/venv/bin/gunicorn bookwyrm.wsgi:application --bind 0.0.0.0:8000
StandardOutput=journal
StandardError=inherit
ProtectSystem=strict
ProtectHome=tmpfs
InaccessiblePaths=-/media -/mnt -/srv
PrivateTmp=yes
TemporaryFileSystem=/var /run /opt
PrivateUsers=true
PrivateDevices=true
BindReadOnlyPaths=/opt/bookwyrm
BindPaths=/opt/bookwyrm/images /opt/bookwyrm/static /var/run/postgresql
LockPersonality=yes
MemoryDenyWriteExecute=true
PrivateMounts=true
ProtectHostname=true
ProtectClock=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
RestrictRealtime=true
RestrictNamespaces=net
[Install]
WantedBy=multi-user.target

View file

@ -1 +1,2 @@
**/vendor/**
**/fonts/**

View file

@ -1,7 +1,7 @@
/* global module */
module.exports = {
"extends": "stylelint-config-standard",
"extends": "stylelint-config-standard-scss",
"plugins": [
"stylelint-order"
@ -18,5 +18,13 @@ module.exports = {
"declaration-block-no-redundant-longhand-properties": null,
"no-descending-specificity": null,
"alpha-value-notation": null
}
},
"overrides": [
{
"files": [ "../**/themes/bookwyrm-*.scss" ],
"rules": {
"no-invalid-position-at-import-rule": null
}
}
]
};

View file

@ -1,14 +1,17 @@
FROM python:3.9
WORKDIR /app/dev-tools
ENV PYTHONUNBUFFERED 1
ENV PATH="/app/dev-tools/node_modules/.bin:$PATH"
ENV PYTHONUNBUFFERED=1
ENV NPM_CONFIG_UPDATE_NOTIFIER=false
ENV PIP_ROOT_USER_ACTION=ignore PIP_DISABLE_PIP_VERSION_CHECK=1
COPY nodejs.sources /etc/apt/sources.list.d/
COPY package.json requirements.txt .stylelintrc.js .stylelintignore /app/dev-tools/
RUN apt-get update && \
apt-get install -y nodejs && \
pip install -r requirements.txt && \
npm install .
RUN mkdir /app
WORKDIR /app
COPY package.json requirements.txt .stylelintrc.js .stylelintignore /app/
RUN pip install -r requirements.txt
RUN apt-get update && apt-get install -y curl
RUN curl -sL https://deb.nodesource.com/setup_18.x | bash -
RUN apt-get install -y nodejs && apt-get clean
RUN npm install .

34
dev-tools/nodejs.sources Normal file
View file

@ -0,0 +1,34 @@
Types: deb
URIs: https://deb.nodesource.com/node_18.x
Suites: nodistro
Components: main
Signed-By:
-----BEGIN PGP PUBLIC KEY BLOCK-----
.
mQENBFdDN1ABCADaNd/I3j3tn40deQNgz7hB2NvT+syXe6k4ZmdiEcOfBvFrkS8B
hNS67t93etHsxEy7E0qwsZH32bKazMqe9zDwoa3aVImryjh6SHC9lMtW27JPHFeM
Srkt9YmH1WMwWcRO6eSY9B3PpazquhnvbammLuUojXRIxkDroy6Fw4UKmUNSRr32
9Ej87jRoR1B2/57Kfp2Y4+vFGGzSvh3AFQpBHq51qsNHALU6+8PjLfIt+5TPvaWR
TB+kAZnQZkaIQM2nr1n3oj6ak2RATY/+kjLizgFWzgEfbCrbsyq68UoY5FPBnu4Z
E3iDZpaIqwKr0seUC7iA1xM5eHi5kty1oB7HABEBAAG0Ik5Tb2xpZCA8bnNvbGlk
LWdwZ0Bub2Rlc291cmNlLmNvbT6JATgEEwECACIFAldDN1ACGwMGCwkIBwMCBhUI
AgkKCwQWAgMBAh4BAheAAAoJEC9ZtfmbG+C0y7wH/i4xnab36dtrYW7RZwL8i6Sc
NjMx4j9+U1kr/F6YtqWd+JwCbBdar5zRghxPcYEq/qf7MbgAYcs1eSOuTOb7n7+o
xUwdH2iCtHhKh3Jr2mRw1ks7BbFZPB5KmkxHaEBfLT4d+I91ZuUdPXJ+0SXs9gzk
Dbz65Uhoz3W03aiF8HeL5JNARZFMbHHNVL05U1sTGTCOtu+1c/33f3TulQ/XZ3Y4
hwGCpLe0Tv7g7Lp3iLMZMWYPEa0a7S4u8he5IEJQLd8bE8jltcQvrdr3Fm8kI2Jg
BJmUmX4PSfhuTCFaR/yeCt3UoW883bs9LfbTzIx9DJGpRIu8Y0IL3b4sj/GoZVq5
AQ0EV0M3UAEIAKrTaC62ayzqOIPa7nS90BHHck4Z33a2tZF/uof38xNOiyWGhT8u
JeFoTTHn5SQq5Ftyu4K3K2fbbpuu/APQF05AaljzVkDGNMW4pSkgOasdysj831cu
ssrHX2RYS22wg80k6C/Hwmh5F45faEuNxsV+bPx7oPUrt5n6GMx84vEP3i1+FDBi
0pt/B/QnDFBXki1BGvJ35f5NwDefK8VaInxXP3ZN/WIbtn5dqxppkV/YkO7GiJlp
Jlju9rf3kKUIQzKQWxFsbCAPIHoWv7rH9RSxgDithXtG6Yg5R1aeBbJaPNXL9wpJ
YBJbiMjkAFaz4B95FOqZm3r7oHugiCGsHX0AEQEAAYkBHwQYAQIACQUCV0M3UAIb
DAAKCRAvWbX5mxvgtE/OB/0VN88DR3Y3fuqy7lq/dthkn7Dqm9YXdorZl3L152eE
IF882aG8FE3qZdaLGjQO4oShAyNWmRfSGuoH0XERXAI9n0r8m4mDMxE6rtP7tHet
y/5M8x3CTyuMgx5GLDaEUvBusnTD+/v/fBMwRK/cZ9du5PSG4R50rtst+oYyC2ao
x4I2SgjtF/cY7bECsZDplzatN3gv34PkcdIg8SLHAVlL4N5tzumDeizRspcSyoy2
K2+hwKU4C4+dekLLTg8rjnRROvplV2KtaEk6rxKtIRFDCoQng8wfJuIMrDNKvqZw
FRGt7cbvW5MCnuH8MhItOl9Uxp1wHp6gtav/h8Gp6MBa
=MARt
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -11,6 +11,7 @@
"stylelint-config-recommended": "^7.0.0",
"stylelint-config-standard": "^25.0.0",
"stylelint-order": "^5.0.0",
"stylelint-config-standard-scss": "^3.0.0",
"watch": "^0.13.0"
},
"dependencies": {

View file

@ -101,6 +101,7 @@ services:
build: dev-tools
env_file: .env
volumes:
- /app/dev-tools/
- .:/app
volumes:
pgdata:

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-27 01:11+0000\n"
"POT-Creation-Date: 2023-10-02 16:40+0000\n"
"PO-Revision-Date: 2021-02-28 17:19-0800\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: English <LL@li.org>\n"
@ -1373,7 +1373,7 @@ msgstr ""
#: bookwyrm/templates/book/editions/editions.html:8
#, python-format
msgid "Editions of <a href=\"%(work_path)s\">\"%(work_title)s\"</a>"
msgid "Editions of <a href=\"%(work_path)s\"><i>%(work_title)s</i></a>"
msgstr ""
#: bookwyrm/templates/book/editions/editions.html:55
@ -2806,14 +2806,8 @@ msgstr ""
#: bookwyrm/templates/import/import.html:21
#, python-format
msgid ""
"\n"
" Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day.\n"
" "
msgid_plural ""
"\n"
" Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days.\n"
" "
msgid "Currently, you are allowed to import %(display_size)s books every %(import_limit_reset)s day."
msgid_plural "Currently, you are allowed to import %(import_size_limit)s books every %(import_limit_reset)s days."
msgstr[0] ""
msgstr[1] ""

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@ environs==9.5.0
flower==1.2.0
libsass==0.22.0
Markdown==3.4.1
Pillow==9.4.0
Pillow==10.0.1
psycopg2==2.9.5
pycryptodome==3.16.0
python-dateutil==2.8.2