Merge branch 'main' into html-in-activitypub
This commit is contained in:
commit
bffde6703c
102 changed files with 4828 additions and 2907 deletions
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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."
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
18
bookwyrm/migrations/0166_sitesettings_imports_enabled.py
Normal file
18
bookwyrm/migrations/0166_sitesettings_imports_enabled.py
Normal file
|
@ -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),
|
||||
),
|
||||
]
|
23
bookwyrm/migrations/0167_auto_20221125_1900.py
Normal file
23
bookwyrm/migrations/0167_auto_20221125_1900.py
Normal file
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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!"""
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -140,6 +140,10 @@ button:focus-visible .button-invisible-overlay {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
button.button-paragraph {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
/** States
|
||||
******************************************************************************/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
{% block about_content %}
|
||||
{# seven day cache #}
|
||||
{% cache 604800 about_page %}
|
||||
{% cache 604800 about_page_superlatives %}
|
||||
|
||||
{% get_book_superlatives as superlatives %}
|
||||
<section class=" pb-4">
|
||||
|
@ -97,6 +97,7 @@
|
|||
</p>
|
||||
|
||||
</section>
|
||||
{% endcache %}
|
||||
|
||||
<section class="block">
|
||||
<header class="content">
|
||||
|
@ -145,5 +146,4 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{% endcache %}
|
||||
{% endblock %}
|
||||
|
|
15
bookwyrm/templates/about/impressum.html
Normal file
15
bookwyrm/templates/about/impressum.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% extends 'about/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Impressum" %}{% endblock %}
|
||||
|
||||
|
||||
{% block about_content %}
|
||||
<div class="block content">
|
||||
<h2>{% trans "Impressum" %}</h2>
|
||||
<div class="content">
|
||||
{{ site.impressum | safe }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -47,6 +47,14 @@
|
|||
{% trans "Privacy Policy" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if site.show_impressum %}
|
||||
<li>
|
||||
{% url 'impressum' as path %}
|
||||
<a href="{{ path }}" {% if request.path in path %}class="is-active"{% endif %}>
|
||||
{% trans "Impressum" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
{% trans "Share this page" %}
|
||||
</span>
|
||||
</summary>
|
||||
<div class="columns mt-3">
|
||||
<div class="columns">
|
||||
<div class="column is-three-fifths is-offset-one-fifth">
|
||||
|
||||
{% if year_key %}
|
||||
|
|
|
@ -144,7 +144,7 @@
|
|||
{% for book in books %}
|
||||
{% with book=book|author_edition:author %}
|
||||
<div class="column is-one-fifth-tablet is-half-mobile is-flex is-flex-direction-column">
|
||||
<div class="is-flex-grow-1">
|
||||
<div class="is-flex-grow-1 mb-3">
|
||||
{% include 'landing/small-book.html' with book=book %}
|
||||
</div>
|
||||
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
|
||||
|
|
|
@ -135,7 +135,7 @@
|
|||
{% trans "View on OpenLibrary" %}
|
||||
</a>
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
<button class="button is-small" type="button" data-modal-open="openlibrary_sync">
|
||||
<button class="button is-small button-paragraph" type="button" data-modal-open="openlibrary_sync">
|
||||
<span class="icon icon-download" title="{{ button_text }}"></span>
|
||||
<span class="is-sr-only-mobile">{{ button_text }}</span>
|
||||
</button>
|
||||
|
@ -150,7 +150,7 @@
|
|||
</a>
|
||||
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
<button class="button is-small" type="button" data-modal-open="inventaire_sync">
|
||||
<button class="button is-small button-paragraph" type="button" data-modal-open="inventaire_sync">
|
||||
<span class="icon icon-download" title="{{ button_text }}"></span>
|
||||
<span class="is-sr-only-mobile">{{ button_text }}</span>
|
||||
</button>
|
||||
|
@ -189,15 +189,15 @@
|
|||
|
||||
{% if user_authenticated and can_edit_book and not book|book_description %}
|
||||
{% trans 'Add Description' as button_text %}
|
||||
{% include 'snippets/toggle/open_button.html' with text=button_text controls_text="add_description" controls_uid=book.id focus="id_description" hide_active=True id="hide_description" %}
|
||||
{% include 'snippets/toggle/open_button.html' with class="mb-2" text=button_text controls_text="add_description" controls_uid=book.id focus="id_description" hide_active=True id="hide_description" %}
|
||||
|
||||
<div class="box is-hidden" id="add_description_{{ book.id }}">
|
||||
<form name="add-description" method="POST" action="{% url "add-description" book.id %}">
|
||||
{% csrf_token %}
|
||||
<p class="fields is-grouped">
|
||||
<div class="field">
|
||||
<label class="label" for="id_description_{{ book.id }}">{% trans "Description:" %}</label>
|
||||
<textarea name="description" cols="None" rows="None" class="textarea" id="id_description_{{ book.id }}"></textarea>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||
{% trans "Cancel" as button_text %}
|
||||
|
@ -231,7 +231,7 @@
|
|||
{% for shelf in user_shelfbooks %}
|
||||
<li class="box">
|
||||
<a href="{{ shelf.shelf.local_path }}">{{ shelf.shelf.name }}</a>
|
||||
<div class="mb-3">
|
||||
<div class="is-pulled-right">
|
||||
{% include 'snippets/shelf_selector.html' with shelf=shelf.shelf class="is-small" readthrough=readthrough %}
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -81,7 +81,7 @@
|
|||
{% include 'snippets/form_errors.html' with errors_list=form.languages.errors id="desc_languages" %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_add_subjects">
|
||||
{% trans "Subjects:" %}
|
||||
</label>
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'snippets/form_errors.html' with errors_list=link.form.availability.errors id="desc_availability" %}
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
{% load layout %}
|
||||
{% load i18n %}
|
||||
{% load sass_tags %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{% get_lang %}">
|
||||
<head>
|
||||
<title>{% block title %}BookWyrm{% endblock %} - {{ site.name }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="{% static "css/vendor/bulma.min.css" %}">
|
||||
<link rel="stylesheet" href="{% static "css/vendor/icons.css" %}">
|
||||
<link rel="stylesheet" href="{% static "css/bookwyrm.css" %}">
|
||||
<link href="{% sass_src site_theme %}" rel="stylesheet" type="text/css" />
|
||||
|
||||
<base target="_blank">
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block filter %}
|
||||
<label class="label mt-2 mb-1">Status types</label>
|
||||
<label class="label mb-1">Status types</label>
|
||||
|
||||
<div class="is-flex is-flex-direction-row is-flex-direction-column-mobile">
|
||||
{% for name, value in feed_status_types_options %}
|
||||
|
|
|
@ -8,83 +8,100 @@
|
|||
<div class="block">
|
||||
<h1 class="title">{% trans "Import Books" %}</h1>
|
||||
|
||||
{% if recent_avg_hours or recent_avg_minutes %}
|
||||
<div class="notification">
|
||||
<p>
|
||||
{% if recent_avg_hours %}
|
||||
{% blocktrans trimmed with hours=recent_avg_hours|floatformat:0|intcomma %}
|
||||
On average, recent imports have taken {{ hours }} hours.
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with minutes=recent_avg_minutes|floatformat:0|intcomma %}
|
||||
On average, recent imports have taken {{ minutes }} minutes.
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if invalid %}
|
||||
<div class="notification is-danger">
|
||||
{% trans "Not a valid CSV file" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form class="box" name="import" action="/import" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% if site.imports_enabled %}
|
||||
{% if recent_avg_hours or recent_avg_minutes %}
|
||||
<div class="notification">
|
||||
<p>
|
||||
{% if recent_avg_hours %}
|
||||
{% blocktrans trimmed with hours=recent_avg_hours|floatformat:0|intcomma %}
|
||||
On average, recent imports have taken {{ hours }} hours.
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with minutes=recent_avg_minutes|floatformat:0|intcomma %}
|
||||
On average, recent imports have taken {{ minutes }} minutes.
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<label class="label" for="source">
|
||||
{% trans "Data source:" %}
|
||||
</label>
|
||||
<form class="box" name="import" action="/import" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="select">
|
||||
<select name="source" id="source" aria-describedby="desc_source">
|
||||
<option value="Goodreads" {% if current == 'Goodreads' %}selected{% endif %}>
|
||||
{% trans "Goodreads (CSV)" %}
|
||||
</option>
|
||||
<option value="Storygraph" {% if current == 'Storygraph' %}selected{% endif %}>
|
||||
{% trans "Storygraph (CSV)" %}
|
||||
</option>
|
||||
<option value="LibraryThing" {% if current == 'LibraryThing' %}selected{% endif %}>
|
||||
{% trans "LibraryThing (TSV)" %}
|
||||
</option>
|
||||
<option value="OpenLibrary" {% if current == 'OpenLibrary' %}selected{% endif %}>
|
||||
{% trans "OpenLibrary (CSV)" %}
|
||||
</option>
|
||||
<option value="Calibre" {% if current == 'Calibre' %}selected{% endif %}>
|
||||
{% trans "Calibre (CSV)" %}
|
||||
</option>
|
||||
</select>
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<label class="label" for="source">
|
||||
{% trans "Data source:" %}
|
||||
</label>
|
||||
|
||||
<div class="select">
|
||||
<select name="source" id="source" aria-describedby="desc_source">
|
||||
<option value="Goodreads" {% if current == 'Goodreads' %}selected{% endif %}>
|
||||
{% trans "Goodreads (CSV)" %}
|
||||
</option>
|
||||
<option value="Storygraph" {% if current == 'Storygraph' %}selected{% endif %}>
|
||||
{% trans "Storygraph (CSV)" %}
|
||||
</option>
|
||||
<option value="LibraryThing" {% if current == 'LibraryThing' %}selected{% endif %}>
|
||||
{% trans "LibraryThing (TSV)" %}
|
||||
</option>
|
||||
<option value="OpenLibrary" {% if current == 'OpenLibrary' %}selected{% endif %}>
|
||||
{% trans "OpenLibrary (CSV)" %}
|
||||
</option>
|
||||
<option value="Calibre" {% if current == 'Calibre' %}selected{% endif %}>
|
||||
{% trans "Calibre (CSV)" %}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<p class="help" id="desc_source">
|
||||
{% blocktrans trimmed %}
|
||||
You can download your Goodreads data from the
|
||||
<a href="https://www.goodreads.com/review/import" target="_blank" rel="nofollow noopener noreferrer">Import/Export page</a>
|
||||
of your Goodreads account.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="help" id="desc_source">
|
||||
{% blocktrans trimmed %}
|
||||
You can download your Goodreads data from the
|
||||
<a href="https://www.goodreads.com/review/import" target="_blank" rel="nofollow noopener noreferrer">Import/Export page</a>
|
||||
of your Goodreads account.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="field">
|
||||
<label class="label" for="id_csv_file">{% trans "Data file:" %}</label>
|
||||
{{ import_form.csv_file }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_csv_file">{% trans "Data file:" %}</label>
|
||||
{{ import_form.csv_file }}
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
<input type="checkbox" name="include_reviews" checked> {% trans "Include reviews" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="privacy_import">
|
||||
{% trans "Privacy setting for imported reviews:" %}
|
||||
</label>
|
||||
{% include 'snippets/privacy_select.html' with no_label=True privacy_uuid="import" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
<input type="checkbox" name="include_reviews" checked> {% trans "Include reviews" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="privacy_import">
|
||||
{% trans "Privacy setting for imported reviews:" %}
|
||||
</label>
|
||||
{% include 'snippets/privacy_select.html' with no_label=True privacy_uuid="import" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="button is-primary" type="submit">{% trans "Import" %}</button>
|
||||
</form>
|
||||
<button class="button is-primary" type="submit">{% trans "Import" %}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="box notification has-text-centered is-warning m-6 content">
|
||||
<p class="mt-5">
|
||||
<span class="icon icon-warning is-size-2" aria-hidden="true"></span>
|
||||
</p>
|
||||
<p class="mb-5">
|
||||
{% trans "Imports are temporarily disabled; thank you for your patience." %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="content block">
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
{% if site.invite_request_question %}
|
||||
<div class="block">
|
||||
<label for="id_answer_register" class="label">{{ site.invite_question_text }}</label>
|
||||
<input type="answer" name="answer" maxlength="50" class="input" required="true" id="id_answer_register" aria-describedby="desc_answer_register">
|
||||
<input type="text" name="answer" maxlength="255" class="input" required="true" id="id_answer_register" aria-describedby="desc_answer_register">
|
||||
{% include 'snippets/form_errors.html' with errors_list=request_form.answer.errors id="desc_answer_register" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
{% load group_tags %}
|
||||
{% load markdown %}
|
||||
|
||||
{% block title %}{% blocktrans with list_name=list.name owner=list.user.display_name %}{{ list_name }}, a list by {{owner}}{% endblocktrans %}{% endblock title %}
|
||||
{% block title %}{% blocktrans trimmed with list_name=list.name owner=list.user.display_name %}
|
||||
{{ list_name }}, a list by {{owner}}
|
||||
{% endblocktrans %}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mt-3">
|
||||
|
|
|
@ -12,12 +12,16 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div class="column is-narrow is-flex">
|
||||
<div class="column is-narrow is-flex field is-grouped">
|
||||
{% if request.user == list.user %}
|
||||
<div class="control">
|
||||
{% trans "Edit List" as button_text %}
|
||||
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit_list" focus="edit_list_header" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include "lists/bookmark_button.html" with list=list %}
|
||||
<div class="control">
|
||||
{% include "lists/bookmark_button.html" with list=list %}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
{% endif %}
|
||||
|
||||
{% if not items.object_list.exists %}
|
||||
<p>{% trans "This list is currently empty" %}</p>
|
||||
<p class="block">{% trans "This list is currently empty." %}</p>
|
||||
{% else %}
|
||||
<ol start="{{ items.start_index }}" class="ordered-list">
|
||||
{% for item in items %}
|
||||
|
|
|
@ -61,7 +61,13 @@
|
|||
{% else %}
|
||||
{% with count=notification.related_list_items.count|add:"-2" %}
|
||||
{% with display_count=count|intcomma %}
|
||||
{% if related_list.curation != "curated" %}
|
||||
{% if count < 1 %}
|
||||
{# This happens if the list item was deleted #}
|
||||
{% blocktrans trimmed %}
|
||||
<a href="{{ related_user_link }}">{{ related_user }}</a>
|
||||
added a book to one of your lists
|
||||
{% endblocktrans %}
|
||||
{% elif related_list.curation != "curated" %}
|
||||
|
||||
{% blocktrans trimmed count counter=count %}
|
||||
<a href="{{ related_user_link }}">{{ related_user }}</a>
|
||||
|
|
|
@ -44,8 +44,31 @@
|
|||
{% csrf_token %}
|
||||
<p>{% trans "Scan the QR code with your authentication app and then enter the code from your app below to confirm your app is set up." %}</p>
|
||||
<div class="columns">
|
||||
<section class="column is-narrow">
|
||||
<figure class="m-4">{{ qrcode | safe }}</figure>
|
||||
<section class="column">
|
||||
<figure class="m-4" id="qrcode">{{ qrcode | safe }}</figure>
|
||||
<details class="details-panel box">
|
||||
<summary>
|
||||
<span role="heading" aria-level="3" class="title is-6">
|
||||
{% trans "Use setup key" %}
|
||||
<span class="details-close icon icon-x" aria-hidden="true"></span>
|
||||
</span>
|
||||
</summary>
|
||||
<dl class="block">
|
||||
<dt class="has-text-weight-bold mr-5 is-pulled-left">
|
||||
{% trans "Account name:" %}
|
||||
</dt>
|
||||
<dd>
|
||||
<code>{{ user.username }}</code>
|
||||
</dd>
|
||||
|
||||
<dt class="has-text-weight-bold mr-5 is-pulled-left">
|
||||
{% trans "Code:" %}
|
||||
</dt>
|
||||
<dd>
|
||||
<code>{{ code | safe }}</code>
|
||||
</dd>
|
||||
</dl>
|
||||
</details>
|
||||
<div class="field">
|
||||
<label class="label" for="id_otp">{% trans "Enter the code from your app:" %}</label>
|
||||
{{ form.otp }}
|
||||
|
|
|
@ -73,6 +73,14 @@ User-agent: PetalBot
|
|||
Disallow: /
|
||||
|
||||
|
||||
User-agent: DataForSeoBot
|
||||
Disallow: /
|
||||
|
||||
User-agent: YisouSpider
|
||||
Disallow: /
|
||||
|
||||
|
||||
User-agent: *
|
||||
Crawl-delay: 10
|
||||
Disallow: /static/js/
|
||||
Disallow: /static/css/
|
||||
|
|
|
@ -1,48 +1,46 @@
|
|||
{% extends 'components/modal.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-title %}
|
||||
{% blocktrans %}
|
||||
Scan Barcode
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="block">
|
||||
<div id="barcode-scanner"></div>
|
||||
</div>
|
||||
|
||||
<div id="barcode-camera-list" class="select is-small">
|
||||
<select>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="barcode-status" class="block">
|
||||
<div class="grant-access is-hidden">
|
||||
<span class="icon icon-lock"></span>
|
||||
<span class="is-size-5">{% trans "Requesting camera..." %}</span><br/>
|
||||
<span>{% trans "Grant access to the camera to scan a book's barcode." %}</span>
|
||||
</div>
|
||||
<div class="access-denied is-hidden">
|
||||
<span class="icon icon-warning"></span>
|
||||
<span class="is-size-5">Access denied</span><br/>
|
||||
<span>{% trans "Could not access camera" %}</span>
|
||||
</div>
|
||||
<div class="scanning is-hidden">
|
||||
<span class="icon icon-barcode"></span>
|
||||
<span class="is-size-5">{% trans "Scanning..." context "barcode scanner" %}</span><br/>
|
||||
<span>{% trans "Align your book's barcode with the camera." %}</span>
|
||||
</div>
|
||||
<div class="found is-hidden">
|
||||
<span class="icon icon-check"></span>
|
||||
<span class="is-size-5">{% trans "ISBN scanned" context "barcode scanner" %}</span><br/>
|
||||
{% trans "Searching for book:" context "followed by ISBN" %} <span class="isbn"></span>...
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button class="button" type="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% extends 'components/modal.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-title %}
|
||||
{% blocktrans %}
|
||||
Scan Barcode
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="block">
|
||||
<div id="barcode-scanner"></div>
|
||||
</div>
|
||||
|
||||
<div id="barcode-camera-list" class="select is-small">
|
||||
<select>
|
||||
</select>
|
||||
</div>
|
||||
<template id="barcode-grant-access">
|
||||
<span class="icon icon-lock"></span>
|
||||
<span class="is-size-5">{% trans "Requesting camera..." %}</span><br/>
|
||||
<span>{% trans "Grant access to the camera to scan a book's barcode." %}</span>
|
||||
</template>
|
||||
<template id="barcode-access-denied">
|
||||
<span class="icon icon-warning"></span>
|
||||
<span class="is-size-5">Access denied</span><br/>
|
||||
<span>{% trans "Could not access camera" %}</span>
|
||||
</template>
|
||||
<template id="barcode-scanning">
|
||||
<span class="icon icon-barcode"></span>
|
||||
<span class="is-size-5">{% trans "Scanning..." context "barcode scanner" %}</span><br/>
|
||||
<span>{% trans "Align your book's barcode with the camera." %}</span>
|
||||
</template>
|
||||
<template id="barcode-found">
|
||||
<span class="icon icon-check"></span>
|
||||
<span class="is-size-5">{% trans "ISBN scanned" context "barcode scanner" %}</span><br/>
|
||||
{% trans "Searching for book:" context "followed by ISBN" %} <span class="isbn"></span>...
|
||||
</template>
|
||||
<div id="barcode-status" class="block"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<button class="button" type="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{% extends 'search/layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load book_display_tags %}
|
||||
|
||||
{% block panel %}
|
||||
|
||||
|
@ -19,8 +21,17 @@
|
|||
</strong>
|
||||
</p>
|
||||
<p>
|
||||
{% with book_review_count=result|review_count %}
|
||||
{% blocktrans trimmed count counter=book_review_count with formatted_review_count=book_review_count|intcomma %}
|
||||
{{ formatted_review_count }} review
|
||||
{% plural %}
|
||||
{{ formatted_review_count }} reviews
|
||||
{% endblocktrans %}
|
||||
{% endwith %}
|
||||
|
||||
{% if result.first_published_date or result.published_date %}
|
||||
({% firstof result.first_published_date.year result.published_date.year %})
|
||||
{% firstof result.first_published_date.year result.published_date.year as pub_year %}
|
||||
{% blocktrans %}(published {{ pub_year }}){% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -47,7 +58,7 @@
|
|||
<span class="details-close icon icon-x" aria-hidden="true"></span>
|
||||
</summary>
|
||||
|
||||
<div class="mt-5">
|
||||
<div>
|
||||
<div class="is-flex is-flex-direction-row-reverse">
|
||||
<ul class="is-flex-grow-1">
|
||||
{% for result in result_set.results %}
|
||||
|
|
|
@ -145,7 +145,7 @@
|
|||
|
||||
<div class="block content">
|
||||
<h2 class="title is-4">{% trans "Current Rules" %}</h2>
|
||||
<details class="details-panel">
|
||||
<details class="details-panel box">
|
||||
<summary>
|
||||
<span class="title is-5" role="heading" aria-level="3">
|
||||
{% trans "Show rules" %} ({{ rules.count }})
|
||||
|
|
|
@ -28,47 +28,49 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tr>
|
||||
{% url 'settings-federation' as url %}
|
||||
<th>
|
||||
{% trans "Instance name" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="server_name" sort=sort text=text %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Date added" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Last updated" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="updated_date" sort=sort text=text %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Software" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="application_type" sort=sort text=text %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Users" %}
|
||||
</th>
|
||||
</tr>
|
||||
{% for server in servers %}
|
||||
<tr>
|
||||
<td><a href="{% url 'settings-federated-server' server.id %}">{{ server.server_name }}</a></td>
|
||||
<td>{{ server.created_date|date:'Y-m-d' }}</td>
|
||||
<td>{{ server.updated_date|date:'Y-m-d' }}</td>
|
||||
<td>
|
||||
{% if server.application_type %}
|
||||
{{ server.application_type }}
|
||||
{% if server.application_version %}({{ server.application_version }}){% endif %}
|
||||
<div class="table-container scroll-x">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tr>
|
||||
{% url 'settings-federation' as url %}
|
||||
<th>
|
||||
{% trans "Instance name" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="server_name" sort=sort text=text %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Date added" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="created_date" sort=sort text=text %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Last updated" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="updated_date" sort=sort text=text %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Software" as text %}
|
||||
{% include 'snippets/table-sort-header.html' with field="application_type" sort=sort text=text %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Users" %}
|
||||
</th>
|
||||
</tr>
|
||||
{% for server in servers %}
|
||||
<tr>
|
||||
<td><a href="{% url 'settings-federated-server' server.id %}">{{ server.server_name }}</a></td>
|
||||
<td>{{ server.created_date|date:'Y-m-d' }}</td>
|
||||
<td>{{ server.updated_date|date:'Y-m-d' }}</td>
|
||||
<td>
|
||||
{% if server.application_type %}
|
||||
{{ server.application_type }}
|
||||
{% if server.application_version %}({{ server.application_version }}){% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ server.user_set.count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not servers %}
|
||||
<tr><td colspan="5"><em>{% trans "No instances found" %}</em></td></tr>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ server.user_set.count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not servers %}
|
||||
<tr><td colspan="5"><em>{% trans "No instances found" %}</em></td></tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
{% include 'snippets/pagination.html' with page=servers path=request.path %}
|
||||
|
|
|
@ -11,6 +11,54 @@
|
|||
|
||||
{% block panel %}
|
||||
|
||||
<div class="block">
|
||||
{% if site.imports_enabled %}
|
||||
<details class="details-panel box">
|
||||
<summary>
|
||||
<span role="heading" aria-level="2" class="title is-6">
|
||||
{% trans "Disable starting new imports" %}
|
||||
</span>
|
||||
<span class="details-close icon icon-x" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<form
|
||||
name="disable-imports"
|
||||
id="disable-imports"
|
||||
method="POST"
|
||||
action="{% url 'settings-imports-disable' %}"
|
||||
>
|
||||
<div class="notification">
|
||||
{% trans "This is only intended to be used when things have gone very wrong with imports and you need to pause the feature while addressing issues." %}
|
||||
{% trans "While imports are disabled, users will not be allowed to start new imports, but existing imports will not be effected." %}
|
||||
</div>
|
||||
{% csrf_token %}
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-danger">
|
||||
{% trans "Disable imports" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
{% else %}
|
||||
<form
|
||||
name="enable-imports"
|
||||
id="enable-imports"
|
||||
method="POST"
|
||||
action="{% url 'settings-imports-enable' %}"
|
||||
class="box"
|
||||
>
|
||||
<div class="notification is-danger is-light">
|
||||
{% trans "Users are currently unable to start new imports" %}
|
||||
</div>
|
||||
{% csrf_token %}
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-success">
|
||||
{% trans "Enable imports" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
|
|
|
@ -40,23 +40,23 @@
|
|||
</h2>
|
||||
</header>
|
||||
<div class="column is-narrow">
|
||||
<button type="button" class="button" data-modal-open="{{ domain_modal }}">
|
||||
<button type="button" class="button is-small" data-modal-open="{{ domain_modal }}">
|
||||
<span class="icon icon-pencil m-0-mobile" aria-hidden="treu"></span>
|
||||
<span class="is-sr-only-mobile">{% trans "Set display name" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<details class="details-panel">
|
||||
<details class="details-panel box">
|
||||
<summary>
|
||||
<span role="heading" aria-level="3" class="title is-6 mb-0">
|
||||
<span role="heading" aria-level="3" class="title is-6">
|
||||
{% trans "View links" %}
|
||||
({{ domain.links.count }})
|
||||
</span>
|
||||
<span class="details-close icon icon-x" aria-hidden="true"></span>
|
||||
</summary>
|
||||
|
||||
<div class="table-container mt-4">
|
||||
<div class="table-container pt-0">
|
||||
{% include "settings/link_domains/link_table.html" with links=domain.links.all|slice:10 %}
|
||||
</div>
|
||||
</details>
|
||||
|
|
|
@ -68,6 +68,19 @@
|
|||
<label class="label" for="id_privacy_policy">{% trans "Privacy Policy:" %}</label>
|
||||
{{ site_form.privacy_policy }}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label" for="id_impressum">{% trans "Impressum:" %}</label>
|
||||
{{ site_form.impressum }}
|
||||
</div>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field mr-2">
|
||||
<label class="label" for="id_show_impressum">{% trans "Include impressum:" %}</label>
|
||||
</div>
|
||||
<div class="control">
|
||||
{{ site_form.show_impressum }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
|
@ -134,7 +134,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="block mt-2">
|
||||
{% include 'shelf/edit_shelf_form.html' with controls_text="edit_shelf_form" %}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<span class="details-close icon icon-x is-{{ size|default:'normal' }}" aria-hidden="true"></span>
|
||||
</summary>
|
||||
|
||||
<div class="mt-3">
|
||||
<div>
|
||||
<form id="filters" method="{{ method|default:'get' }}" action="{{ action|default:request.path }}">
|
||||
{% if method == 'post' %}
|
||||
{% csrf_token %}
|
||||
|
@ -34,7 +34,7 @@
|
|||
{% if sort %}
|
||||
<input type="hidden" name="sort" value="{{ sort }}">
|
||||
{% endif %}
|
||||
<div class="mt-3 columns filters-fields is-align-items-stretch">
|
||||
<div class="columns filters-fields is-align-items-stretch">
|
||||
{% block filter_fields %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
|
|
@ -24,11 +24,16 @@
|
|||
</div>
|
||||
<div class="column is-2">
|
||||
<p>
|
||||
<a href ="{% url 'privacy' %}">{% trans "Code of Conduct" %}</a>
|
||||
<a href ="{% url 'conduct' %}">{% trans "Code of Conduct" %}</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href ="{% url 'privacy' %}">{% trans "Privacy Policy" %}</a>
|
||||
</p>
|
||||
{% if site.show_impressum %}
|
||||
<p>
|
||||
<a href ="{% url 'impressum' %}">{% trans "Impressum" %}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="column content">
|
||||
{% if site.support_link %}
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<div class="card-footer-item">
|
||||
{% trans "Reply" as button_text %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with controls_text="show_comment" controls_uid=status.id text=button_text icon_with_text="comment" class="is-small is-light toggle-button" focus="id_content_reply" %}
|
||||
{% include 'snippets/toggle/toggle_button.html' with controls_text="show_comment" controls_uid=status.id text=button_text icon_with_text="comment" class="is-small is-light is-transparent toggle-button" focus="id_content_reply" %}
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
{% include 'snippets/boost_button.html' with status=status %}
|
||||
|
@ -42,7 +42,7 @@
|
|||
</div>
|
||||
{% if not moderation_mode %}
|
||||
<div class="card-footer-item">
|
||||
{% include 'snippets/status/status_options.html' with class="is-small is-light" right=True %}
|
||||
{% include 'snippets/status/status_options.html' with class="is-small is-light is-transparent" right=True %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -64,12 +64,14 @@
|
|||
<div>
|
||||
<div class="columns is-mobile">
|
||||
<h2 class="title column">{% trans "User Activity" %}</h2>
|
||||
{% if user.local %}
|
||||
<div class="column is-narrow">
|
||||
<a target="_blank" href="{{ user.local_path }}/rss" rel="nofollow noopener noreferrer">
|
||||
<span class="icon icon-rss" aria-hidden="true"></span>
|
||||
<span class="is-hidden-mobile">{% trans "RSS feed" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% for activity in activities %}
|
||||
<div class="block" id="feed_{{ activity.id }}">
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
""" template filters """
|
||||
from django import template
|
||||
from bookwyrm import models
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(name="review_count")
|
||||
def get_review_count(book):
|
||||
"""how many reviews?"""
|
||||
return models.Review.objects.filter(deleted=False, book=book).count()
|
||||
|
||||
|
||||
@register.filter(name="book_description")
|
||||
def get_book_description(book):
|
||||
"""use the work's text if the book doesn't have it"""
|
||||
|
|
|
@ -42,7 +42,7 @@ def get_relationship(context, user_object):
|
|||
"""caches the relationship between the logged in user and another user"""
|
||||
user = context["request"].user
|
||||
return get_or_set(
|
||||
f"relationship-{user.id}-{user_object.id}",
|
||||
f"cached-relationship-{user.id}-{user_object.id}",
|
||||
get_relationship_name,
|
||||
user,
|
||||
user_object,
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
Title,Authors,Contributors,ISBN,Format,Read Status,Date Added,Last Date Read,Dates Read,Read Count,Moods,Pace,Character- or Plot-Driven?,Strong Character Development?,Loveable Characters?,Diverse Characters?,Flawed Characters?,Star Rating,Review,Content Warnings,Content Warning Description,Tags,Owned?
|
||||
Always Coming Home,"Ursula K. Le Guin, Todd Barton, Margaret Chodos-Irvine","",,,to-read,2021/05/10,"","",0,"",,,,,,,,,"",,"",No
|
||||
Subprime Attention Crisis,Tim Hwang,"",,,read,2021/05/10,"","",1,informative,fast,,,,,,5.0,"","","","",No
|
||||
Title,Authors,Contributors,ISBN/UID,Format,Read Status,Date Added,Last Date Read,Dates Read,Read Count,Moods,Pace,Character- or Plot-Driven?,Strong Character Development?,Loveable Characters?,Diverse Characters?,Flawed Characters?,Star Rating,Review,Content Warnings,Content Warning Description,Tags,Owned?
|
||||
Always Coming Home,"Ursula K. Le Guin, Todd Barton, Margaret Chodos-Irvine","",9780520227354,,to-read,2021/05/10,"","",0,"",,,,,,,,,"",,"",No
|
||||
Subprime Attention Crisis,Tim Hwang,"",0374538654,,read,2021/05/10,"","",1,informative,fast,,,,,,5.0,"","","","",No
|
||||
|
|
|
|
@ -53,13 +53,19 @@ class StorygraphImport(TestCase):
|
|||
models.ImportItem.objects.filter(job=import_job).order_by("index").all()
|
||||
)
|
||||
self.assertEqual(len(import_items), 2)
|
||||
self.assertEqual(import_items[0].index, 0)
|
||||
self.assertEqual(import_items[0].normalized_data["title"], "Always Coming Home")
|
||||
self.assertEqual(import_items[1].index, 1)
|
||||
|
||||
always_book = import_items[0]
|
||||
self.assertEqual(always_book.index, 0)
|
||||
self.assertEqual(always_book.normalized_data["title"], "Always Coming Home")
|
||||
self.assertEqual(always_book.isbn, "9780520227354")
|
||||
|
||||
subprime_book = import_items[1]
|
||||
self.assertEqual(subprime_book.index, 1)
|
||||
self.assertEqual(
|
||||
import_items[1].normalized_data["title"], "Subprime Attention Crisis"
|
||||
subprime_book.normalized_data["title"], "Subprime Attention Crisis"
|
||||
)
|
||||
self.assertEqual(import_items[1].normalized_data["rating"], "5.0")
|
||||
self.assertEqual(subprime_book.normalized_data["rating"], "5.0")
|
||||
self.assertEqual(subprime_book.isbn, "0374538654")
|
||||
|
||||
def test_handle_imported_book(self, *_):
|
||||
"""storygraph import added a book, this adds related connections"""
|
||||
|
|
|
@ -191,14 +191,14 @@ class Status(TestCase):
|
|||
self.assertEqual(activity["type"], "Note")
|
||||
self.assertEqual(activity["sensitive"], False)
|
||||
self.assertIsInstance(activity["attachment"], list)
|
||||
self.assertEqual(activity["attachment"][0].type, "Document")
|
||||
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
||||
self.assertTrue(
|
||||
re.match(
|
||||
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
|
||||
activity["attachment"][0].url,
|
||||
activity["attachment"][0]["url"],
|
||||
)
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].name, "Test Edition")
|
||||
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
||||
|
||||
def test_comment_to_activity(self, *_):
|
||||
"""subclass of the base model version with a "pure" serializer"""
|
||||
|
@ -223,14 +223,14 @@ class Status(TestCase):
|
|||
activity["content"],
|
||||
f'test content<p>(comment on <a href="{self.book.remote_id}">"Test Edition"</a>)</p>',
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].type, "Document")
|
||||
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
||||
# self.assertTrue(
|
||||
# re.match(
|
||||
# r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
|
||||
# activity["attachment"][0].url,
|
||||
# )
|
||||
# )
|
||||
self.assertEqual(activity["attachment"][0].name, "Test Edition")
|
||||
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
||||
|
||||
def test_quotation_to_activity(self, *_):
|
||||
"""subclass of the base model version with a "pure" serializer"""
|
||||
|
@ -262,14 +262,14 @@ class Status(TestCase):
|
|||
activity["content"],
|
||||
f'a sickening sense <p>-- <a href="{self.book.remote_id}">"Test Edition"</a></p>test content',
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].type, "Document")
|
||||
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
||||
self.assertTrue(
|
||||
re.match(
|
||||
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
|
||||
activity["attachment"][0].url,
|
||||
activity["attachment"][0]["url"],
|
||||
)
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].name, "Test Edition")
|
||||
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
||||
|
||||
def test_review_to_activity(self, *_):
|
||||
"""subclass of the base model version with a "pure" serializer"""
|
||||
|
@ -305,14 +305,14 @@ class Status(TestCase):
|
|||
f'Review of "{self.book.title}" (3 stars): Review\'s name',
|
||||
)
|
||||
self.assertEqual(activity["content"], "test content")
|
||||
self.assertEqual(activity["attachment"][0].type, "Document")
|
||||
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
||||
self.assertTrue(
|
||||
re.match(
|
||||
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
|
||||
activity["attachment"][0].url,
|
||||
activity["attachment"][0]["url"],
|
||||
)
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].name, "Test Edition")
|
||||
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
||||
|
||||
def test_review_to_pure_activity_no_rating(self, *_):
|
||||
"""subclass of the base model version with a "pure" serializer"""
|
||||
|
@ -330,14 +330,14 @@ class Status(TestCase):
|
|||
f'Review of "{self.book.title}": Review name',
|
||||
)
|
||||
self.assertEqual(activity["content"], "test content")
|
||||
self.assertEqual(activity["attachment"][0].type, "Document")
|
||||
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
||||
self.assertTrue(
|
||||
re.match(
|
||||
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
|
||||
activity["attachment"][0].url,
|
||||
activity["attachment"][0]["url"],
|
||||
)
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].name, "Test Edition")
|
||||
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
||||
|
||||
def test_reviewrating_to_pure_activity(self, *_):
|
||||
"""subclass of the base model version with a "pure" serializer"""
|
||||
|
@ -353,14 +353,14 @@ class Status(TestCase):
|
|||
activity["content"],
|
||||
f'rated <em><a href="{self.book.remote_id}">{self.book.title}</a></em>: 3 stars',
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].type, "Document")
|
||||
self.assertEqual(activity["attachment"][0]["type"], "Document")
|
||||
self.assertTrue(
|
||||
re.match(
|
||||
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
|
||||
activity["attachment"][0].url,
|
||||
activity["attachment"][0]["url"],
|
||||
)
|
||||
)
|
||||
self.assertEqual(activity["attachment"][0].name, "Test Edition")
|
||||
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
|
||||
|
||||
def test_favorite(self, *_):
|
||||
"""fav a status"""
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
""" testing models """
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
from django.contrib.auth.models import Group
|
||||
from django.test import TestCase
|
||||
import responses
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.management.commands import initdb
|
||||
from bookwyrm.settings import USE_HTTPS, DOMAIN
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
|
@ -12,6 +14,7 @@ from bookwyrm.settings import USE_HTTPS, DOMAIN
|
|||
class User(TestCase):
|
||||
protocol = "https://" if USE_HTTPS else "http://"
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def setUp(self):
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||
|
@ -25,6 +28,17 @@ class User(TestCase):
|
|||
name="hi",
|
||||
bookwyrm_user=False,
|
||||
)
|
||||
self.another_user = models.User.objects.create_user(
|
||||
f"nutria@{DOMAIN}",
|
||||
"nutria@nutria.nutria",
|
||||
"nutriaword",
|
||||
local=True,
|
||||
localname="nutria",
|
||||
name="hi",
|
||||
bookwyrm_user=False,
|
||||
)
|
||||
initdb.init_groups()
|
||||
initdb.init_permissions()
|
||||
|
||||
def test_computed_fields(self):
|
||||
"""username instead of id here"""
|
||||
|
@ -176,3 +190,41 @@ class User(TestCase):
|
|||
self.assertEqual(activity["type"], "Delete")
|
||||
self.assertEqual(activity["object"], self.user.remote_id)
|
||||
self.assertFalse(self.user.is_active)
|
||||
|
||||
def test_admins_no_admins(self):
|
||||
"""list of admins"""
|
||||
result = models.User.admins()
|
||||
self.assertFalse(result.exists())
|
||||
|
||||
def test_admins_superuser(self):
|
||||
"""list of admins"""
|
||||
self.user.is_superuser = True
|
||||
self.user.save(broadcast=False, update_fields=["is_superuser"])
|
||||
result = models.User.admins()
|
||||
self.assertEqual(result.count(), 1)
|
||||
self.assertEqual(result.first(), self.user)
|
||||
|
||||
def test_admins_superuser_and_mod(self):
|
||||
"""list of admins"""
|
||||
self.user.is_superuser = True
|
||||
self.user.save(broadcast=False, update_fields=["is_superuser"])
|
||||
group = Group.objects.get(name="moderator")
|
||||
self.another_user.groups.set([group])
|
||||
|
||||
results = models.User.admins()
|
||||
self.assertEqual(results.count(), 2)
|
||||
self.assertTrue(results.filter(id=self.user.id).exists())
|
||||
self.assertTrue(results.filter(id=self.another_user.id).exists())
|
||||
|
||||
def test_admins_deleted_mod(self):
|
||||
"""list of admins"""
|
||||
self.user.is_superuser = True
|
||||
self.user.save(broadcast=False, update_fields=["is_superuser"])
|
||||
group = Group.objects.get(name="moderator")
|
||||
self.another_user.groups.set([group])
|
||||
self.another_user.is_active = False
|
||||
self.another_user.save(broadcast=False, update_fields=None)
|
||||
|
||||
results = models.User.admins()
|
||||
self.assertEqual(results.count(), 1)
|
||||
self.assertEqual(results.first(), self.user)
|
||||
|
|
|
@ -11,6 +11,7 @@ from bookwyrm import emailing, models
|
|||
class Emailing(TestCase):
|
||||
"""every response to a get request, html or json"""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
self.factory = RequestFactory()
|
||||
|
@ -41,10 +42,12 @@ class Emailing(TestCase):
|
|||
self.assertEqual(args[1], "You're invited to join BookWyrm!")
|
||||
self.assertEqual(len(args), 4)
|
||||
|
||||
def test_password_reset_email(self, email_mock):
|
||||
def test_password_reset_email(self, _):
|
||||
"""load the password reset email"""
|
||||
reset = models.PasswordReset.objects.create(user=self.local_user)
|
||||
emailing.password_reset_email(reset)
|
||||
|
||||
with patch("bookwyrm.emailing.send_email") as email_mock:
|
||||
emailing.password_reset_email(reset)
|
||||
|
||||
self.assertEqual(email_mock.call_count, 1)
|
||||
args = email_mock.call_args[0]
|
||||
|
|
|
@ -14,6 +14,7 @@ from bookwyrm.tests.validate_html import validate_html
|
|||
class SiteSettingsViews(TestCase):
|
||||
"""Edit site settings"""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
self.factory = RequestFactory()
|
||||
|
@ -56,6 +57,8 @@ class SiteSettingsViews(TestCase):
|
|||
form.data["invite_request_text"] = "blah"
|
||||
form.data["code_of_conduct"] = "blah"
|
||||
form.data["privacy_policy"] = "blah"
|
||||
form.data["show_impressum"] = False
|
||||
form.data["impressum"] = "bleh"
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ from bookwyrm.tests.validate_html import validate_html
|
|||
class LinkViews(TestCase):
|
||||
"""books books books"""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
self.factory = RequestFactory()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.http import Http404
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
@ -77,6 +78,28 @@ class LandingViews(TestCase):
|
|||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_impressum_page_off(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.impressum
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
with self.assertRaises(Http404):
|
||||
view(request)
|
||||
|
||||
def test_impressum_page_on(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
site = models.SiteSettings.objects.get()
|
||||
site.show_impressum = True
|
||||
site.save()
|
||||
|
||||
view = views.impressum
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_landing(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.Landing.as_view()
|
||||
|
|
|
@ -301,6 +301,16 @@ urlpatterns = [
|
|||
views.ImportList.as_view(),
|
||||
name="settings-imports-complete",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/imports/disable/?$",
|
||||
views.disable_imports,
|
||||
name="settings-imports-disable",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/imports/enable/?$",
|
||||
views.enable_imports,
|
||||
name="settings-imports-enable",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/celery/?$", views.CeleryStatus.as_view(), name="settings-celery"
|
||||
),
|
||||
|
@ -308,6 +318,7 @@ urlpatterns = [
|
|||
re_path(r"^about/?$", views.about, name="about"),
|
||||
re_path(r"^privacy/?$", views.privacy, name="privacy"),
|
||||
re_path(r"^conduct/?$", views.conduct, name="conduct"),
|
||||
re_path(r"^impressum/?$", views.impressum, name="impressum"),
|
||||
path("", views.Home.as_view(), name="landing"),
|
||||
re_path(r"^discover/?$", views.Discover.as_view(), name="discover"),
|
||||
re_path(r"^notifications/?$", views.Notifications.as_view(), name="notifications"),
|
||||
|
|
|
@ -10,7 +10,7 @@ from .admin.federation import Federation, FederatedServer
|
|||
from .admin.federation import AddFederatedServer, ImportServerBlocklist
|
||||
from .admin.federation import block_server, unblock_server, refresh_server
|
||||
from .admin.email_blocklist import EmailBlocklist
|
||||
from .admin.imports import ImportList
|
||||
from .admin.imports import ImportList, disable_imports, enable_imports
|
||||
from .admin.ip_blocklist import IPBlocklist
|
||||
from .admin.invite import ManageInvites, Invite, InviteRequest
|
||||
from .admin.invite import ManageInviteRequests, ignore_invite_request
|
||||
|
@ -60,7 +60,7 @@ from .books.editions import Editions, switch_edition
|
|||
from .books.links import BookFileLinks, AddFileLink, delete_link
|
||||
|
||||
# landing
|
||||
from .landing.about import about, privacy, conduct
|
||||
from .landing.about import about, privacy, conduct, impressum
|
||||
from .landing.landing import Home, Landing
|
||||
from .landing.login import Login, Logout
|
||||
from .landing.register import Register
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.shortcuts import get_object_or_404, redirect
|
|||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
|
@ -53,3 +54,25 @@ class ImportList(View):
|
|||
import_job = get_object_or_404(models.ImportJob, id=import_id)
|
||||
import_job.stop_job()
|
||||
return redirect("settings-imports")
|
||||
|
||||
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def disable_imports(request):
|
||||
"""When you just need people to please stop starting imports"""
|
||||
site = models.SiteSettings.objects.get()
|
||||
site.imports_enabled = False
|
||||
site.save(update_fields=["imports_enabled"])
|
||||
return redirect("settings-imports")
|
||||
|
||||
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def enable_imports(request):
|
||||
"""When you just need people to please stop starting imports"""
|
||||
site = models.SiteSettings.objects.get()
|
||||
site.imports_enabled = True
|
||||
site.save(update_fields=["imports_enabled"])
|
||||
return redirect("settings-imports")
|
||||
|
|
|
@ -21,11 +21,7 @@ class BookFileLinks(View):
|
|||
def get(self, request, book_id):
|
||||
"""view links"""
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
links = book.file_links.order_by("domain__status", "created_date")
|
||||
annotated_links = []
|
||||
for link in links.all():
|
||||
link.form = forms.FileLinkForm(instance=link)
|
||||
annotated_links.append(link)
|
||||
annotated_links = get_annotated_links(book)
|
||||
|
||||
data = {"book": book, "links": annotated_links}
|
||||
return TemplateResponse(request, "book/file_links/edit_links.html", data)
|
||||
|
@ -34,8 +30,30 @@ class BookFileLinks(View):
|
|||
"""Edit a link"""
|
||||
link = get_object_or_404(models.FileLink, id=link_id, book=book_id)
|
||||
form = forms.FileLinkForm(request.POST, instance=link)
|
||||
form.save(request)
|
||||
return self.get(request, book_id)
|
||||
if form.is_valid():
|
||||
form.save(request)
|
||||
return redirect("file-link", book_id)
|
||||
|
||||
# this form shouldn't ever really get here, since it's just a dropdown
|
||||
# get the data again rather than redirecting
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
annotated_links = get_annotated_links(book, form=form)
|
||||
|
||||
data = {"book": book, "links": annotated_links}
|
||||
return TemplateResponse(request, "book/file_links/edit_links.html", data)
|
||||
|
||||
|
||||
def get_annotated_links(book, form=None):
|
||||
"""The links for this book, plus the forms to edit those links"""
|
||||
links = book.file_links.order_by("domain__status", "created_date")
|
||||
annotated_links = []
|
||||
for link in links.all():
|
||||
if form and link.id == form.instance.id:
|
||||
link.form = form
|
||||
else:
|
||||
link.form = forms.FileLinkForm(instance=link)
|
||||
annotated_links.append(link)
|
||||
return annotated_links
|
||||
|
||||
|
||||
@require_POST
|
||||
|
|
|
@ -4,13 +4,13 @@ import datetime
|
|||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Avg, ExpressionWrapper, F, fields
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.paginator import Paginator
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.shortcuts import redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
|
||||
from bookwyrm import forms, models
|
||||
|
@ -29,7 +29,7 @@ from bookwyrm.utils.cache import get_or_set
|
|||
class Import(View):
|
||||
"""import view"""
|
||||
|
||||
def get(self, request):
|
||||
def get(self, request, invalid=False):
|
||||
"""load import page"""
|
||||
jobs = models.ImportJob.objects.filter(user=request.user).order_by(
|
||||
"-created_date"
|
||||
|
@ -42,6 +42,7 @@ class Import(View):
|
|||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
"invalid": invalid,
|
||||
}
|
||||
|
||||
seconds = get_or_set("avg-import-time", get_average_import_time, timeout=86400)
|
||||
|
@ -54,6 +55,10 @@ class Import(View):
|
|||
|
||||
def post(self, request):
|
||||
"""ingest a goodreads csv"""
|
||||
site = models.SiteSettings.objects.get()
|
||||
if not site.imports_enabled:
|
||||
raise PermissionDenied()
|
||||
|
||||
form = forms.ImportForm(request.POST, request.FILES)
|
||||
if not form.is_valid():
|
||||
return HttpResponseBadRequest()
|
||||
|
@ -83,7 +88,7 @@ class Import(View):
|
|||
privacy,
|
||||
)
|
||||
except (UnicodeDecodeError, ValueError, KeyError):
|
||||
return HttpResponseBadRequest(_("Not a valid csv file"))
|
||||
return self.get(request, invalid=True)
|
||||
|
||||
job.start_job()
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
""" non-interactive pages """
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.http import Http404
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.http import require_GET
|
||||
|
@ -36,3 +37,12 @@ def conduct(request):
|
|||
def privacy(request):
|
||||
"""more information about the instance"""
|
||||
return TemplateResponse(request, "about/privacy.html")
|
||||
|
||||
|
||||
@require_GET
|
||||
def impressum(request):
|
||||
"""more information about the instance"""
|
||||
site = models.SiteSettings.objects.get()
|
||||
if not site.show_impressum:
|
||||
raise Http404()
|
||||
return TemplateResponse(request, "about/impressum.html")
|
||||
|
|
|
@ -110,8 +110,8 @@ def get_list_suggestions(book_list, user, query=None):
|
|||
s.default_edition
|
||||
for s in models.Work.objects.filter(
|
||||
~Q(editions__in=book_list.books.all()),
|
||||
).order_by("-updated_date")
|
||||
][: 5 - len(suggestions)]
|
||||
).order_by("-updated_date")[: 5 - len(suggestions)]
|
||||
]
|
||||
return suggestions
|
||||
|
||||
|
||||
|
|
|
@ -35,10 +35,12 @@ class Edit2FA(View):
|
|||
if not form.is_valid():
|
||||
data = {"form": form}
|
||||
return TemplateResponse(request, "preferences/2fa.html", data)
|
||||
data = self.create_qr_code(request.user)
|
||||
qr_form = forms.Confirm2FAForm()
|
||||
data = {
|
||||
"password_confirmed": True,
|
||||
"qrcode": self.create_qr_code(request.user),
|
||||
"qrcode": data[0],
|
||||
"code": data[1],
|
||||
"form": qr_form,
|
||||
}
|
||||
return TemplateResponse(request, "preferences/2fa.html", data)
|
||||
|
@ -57,7 +59,10 @@ class Edit2FA(View):
|
|||
qr_code.add_data(provisioning_url)
|
||||
qr_code.make(fit=True)
|
||||
img = qr_code.make_image(attrib={"fill": "black"})
|
||||
return str(img.to_string(), "utf-8") # to_string() returns a byte string
|
||||
return [
|
||||
str(img.to_string(), "utf-8"),
|
||||
otp_secret,
|
||||
] # to_string() returns a byte string
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue