Merge branch 'main' into user-migration
This commit is contained in:
commit
8477d0b89d
91 changed files with 6697 additions and 3892 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
@charset "utf-8";
|
||||
|
||||
@import "vendor/bulma/bulma.sass";
|
||||
@import "bookwyrm/all.scss";
|
||||
@import "vendor/bulma/bulma";
|
||||
@import "bookwyrm/all";
|
||||
|
|
|
@ -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
|
||||
******************************************************************************/
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
max-height: 100%;
|
||||
|
||||
/* Useful when stretching under-sized images. */
|
||||
image-rendering: optimizeQuality;
|
||||
image-rendering: optimizequality;
|
||||
image-rendering: smooth;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Binary file not shown.
|
@ -43,6 +43,8 @@
|
|||
<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="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="" 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="" 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="" 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="" 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 |
Binary file not shown.
Binary file not shown.
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
6
bookwyrm/static/css/vendor/icons.css
vendored
6
bookwyrm/static/css/vendor/icons.css
vendored
|
@ -155,3 +155,9 @@
|
|||
.icon-barcode:before {
|
||||
content: "\e937";
|
||||
}
|
||||
.icon-eye:before {
|
||||
content: "\e9ce";
|
||||
}
|
||||
.icon-eye-blocked:before {
|
||||
content: "\e9d1";
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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. We’ll 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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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">
|
||||
|
|
14
bookwyrm/templates/manifest.json
Normal file
14
bookwyrm/templates/manifest.json
Normal 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"
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue