1
0
Fork 0

Merge branch 'main' into import-limit

This commit is contained in:
Mouse Reeve 2022-12-16 12:44:57 -08:00 committed by GitHub
commit d4351cfcb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
121 changed files with 5402 additions and 2951 deletions

View file

@ -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}
@ -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)

View file

@ -19,6 +19,8 @@ class BookData(ActivityObject):
viaf: str = None
wikidata: str = None
asin: str = None
aasin: str = None
isfdb: str = None
lastEditedBy: str = None
links: List[str] = field(default_factory=lambda: [])
fileLinks: List[str] = field(default_factory=lambda: [])

View file

@ -18,6 +18,12 @@ def email_data():
}
def test_email(user):
"""Just an admin checking if emails are sending"""
data = email_data()
send_email(user.email, *format_email("test", data))
def email_confirmation_email(user):
"""newly registered users confirm email address"""
data = email_data()

View file

@ -55,11 +55,45 @@ class CreateInviteForm(CustomForm):
class SiteForm(CustomForm):
class Meta:
model = models.SiteSettings
exclude = ["admin_code", "install_mode"]
fields = [
"name",
"instance_tagline",
"instance_description",
"instance_short_description",
"default_theme",
"code_of_conduct",
"privacy_policy",
"impressum",
"show_impressum",
"logo",
"logo_small",
"favicon",
"support_link",
"support_title",
"admin_email",
"footer_item",
]
widgets = {
"instance_short_description": forms.TextInput(
attrs={"aria-describedby": "desc_instance_short_description"}
),
}
class RegistrationForm(CustomForm):
class Meta:
model = models.SiteSettings
fields = [
"allow_registration",
"allow_invite_requests",
"registration_closed_text",
"invite_request_text",
"invite_request_question",
"invite_question_text",
"require_confirm_email",
]
widgets = {
"require_confirm_email": forms.CheckboxInput(
attrs={"aria-describedby": "desc_require_confirm_email"}
),
@ -69,6 +103,23 @@ class SiteForm(CustomForm):
}
class RegistrationLimitedForm(CustomForm):
class Meta:
model = models.SiteSettings
fields = [
"registration_closed_text",
"invite_request_text",
"invite_request_question",
"invite_question_text",
]
widgets = {
"invite_request_text": forms.Textarea(
attrs={"aria-describedby": "desc_invite_request_text"}
),
}
class ThemeForm(CustomForm):
class Meta:
model = models.Theme

View file

@ -21,6 +21,7 @@ class AuthorForm(CustomForm):
"inventaire_id",
"librarything_key",
"goodreads_key",
"isfdb",
"isni",
]
widgets = {

View file

@ -18,19 +18,30 @@ class CoverForm(CustomForm):
class EditionForm(CustomForm):
class Meta:
model = models.Edition
exclude = [
"remote_id",
"origin_id",
"created_date",
"updated_date",
"edition_rank",
"authors",
"parent_work",
"shelves",
"connector",
"search_vector",
"links",
"file_links",
fields = [
"title",
"subtitle",
"description",
"series",
"series_number",
"languages",
"subjects",
"publishers",
"first_published_date",
"published_date",
"cover",
"physical_format",
"physical_format_detail",
"pages",
"isbn_13",
"isbn_10",
"openlibrary_key",
"inventaire_id",
"goodreads_key",
"oclc_number",
"asin",
"aasin",
"isfdb",
]
widgets = {
"title": forms.TextInput(attrs={"aria-describedby": "desc_title"}),
@ -73,10 +84,15 @@ class EditionForm(CustomForm):
"inventaire_id": forms.TextInput(
attrs={"aria-describedby": "desc_inventaire_id"}
),
"goodreads_key": forms.TextInput(
attrs={"aria-describedby": "desc_goodreads_key"}
),
"oclc_number": forms.TextInput(
attrs={"aria-describedby": "desc_oclc_number"}
),
"ASIN": forms.TextInput(attrs={"aria-describedby": "desc_ASIN"}),
"AASIN": forms.TextInput(attrs={"aria-describedby": "desc_AASIN"}),
"isfdb": forms.TextInput(attrs={"aria-describedby": "desc_isfdb"}),
}

View file

@ -17,8 +17,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"]),

View file

@ -0,0 +1,19 @@
""" manually confirm e-mail of user """
from django.core.management.base import BaseCommand
from bookwyrm import models
class Command(BaseCommand):
"""command-line options"""
help = "Manually confirm email for user"
def add_arguments(self, parser):
parser.add_argument("username")
def handle(self, *args, **options):
name = options["username"]
user = models.User.objects.get(localname=name)
user.reactivate()
self.stdout.write(self.style.SUCCESS("User's email is now confirmed."))

View file

@ -8,54 +8,64 @@ from bookwyrm import models
def init_groups():
"""permission levels"""
groups = ["admin", "moderator", "editor"]
groups = ["admin", "owner", "moderator", "editor"]
for group in groups:
Group.objects.create(name=group)
Group.objects.get_or_create(name=group)
def init_permissions():
"""permission types"""
permissions = [
{
"codename": "manage_registration",
"name": "allow or prevent user registration",
"groups": ["admin"],
},
{
"codename": "system_administration",
"name": "technical controls",
"groups": ["admin"],
},
{
"codename": "edit_instance_settings",
"name": "change the instance info",
"groups": ["admin"],
"groups": ["admin", "owner"],
},
{
"codename": "set_user_group",
"name": "change what group a user is in",
"groups": ["admin", "moderator"],
"groups": ["admin", "owner", "moderator"],
},
{
"codename": "control_federation",
"name": "control who to federate with",
"groups": ["admin", "moderator"],
"groups": ["admin", "owner", "moderator"],
},
{
"codename": "create_invites",
"name": "issue invitations to join",
"groups": ["admin", "moderator"],
"groups": ["admin", "owner", "moderator"],
},
{
"codename": "moderate_user",
"name": "deactivate or silence a user",
"groups": ["admin", "moderator"],
"groups": ["admin", "owner", "moderator"],
},
{
"codename": "moderate_post",
"name": "delete other users' posts",
"groups": ["admin", "moderator"],
"groups": ["admin", "owner", "moderator"],
},
{
"codename": "edit_book",
"name": "edit book info",
"groups": ["admin", "moderator", "editor"],
"groups": ["admin", "owner", "moderator", "editor"],
},
]
content_type = ContentType.objects.get_for_model(models.User)
for permission in permissions:
permission_obj = Permission.objects.create(
permission_obj, _ = Permission.objects.get_or_create(
codename=permission["codename"],
name=permission["name"],
content_type=content_type,

View 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),
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 3.2.16 on 2022-12-05 17:01
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0167_auto_20221125_1900"),
]
operations = [
migrations.AddField(
model_name="author",
name="aasin",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
migrations.AddField(
model_name="book",
name="aasin",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
]

View file

@ -0,0 +1,63 @@
""" I added two new permission types and a new group to the management command that
creates the database on install, this creates them for existing instances """
# Generated by Django 3.2.16 on 2022-12-05 23:31
from django.db import migrations
def create_groups_and_perms(apps, schema_editor):
"""create the new "owner" group and "system admin" permission"""
db_alias = schema_editor.connection.alias
group_model = apps.get_model("auth", "Group")
# Add the "owner" group, if needed
owner_group, group_created = group_model.objects.using(db_alias).get_or_create(
name="owner"
)
# Create perms, if needed
user_model = apps.get_model("bookwyrm", "User")
content_type_model = apps.get_model("contenttypes", "ContentType")
content_type = content_type_model.objects.get_for_model(user_model)
perms_model = apps.get_model("auth", "Permission")
reg_perm, perm_created = perms_model.objects.using(db_alias).get_or_create(
codename="manage_registration",
name="allow or prevent user registration",
content_type=content_type,
)
admin_perm, admin_perm_created = perms_model.objects.using(db_alias).get_or_create(
codename="system_administration",
name="technical controls",
content_type=content_type,
)
# Add perms to the group if anything was created
if group_created or perm_created or admin_perm_created:
perms = [
"edit_instance_settings",
"set_user_group",
"control_federation",
"create_invites",
"moderate_user",
"moderate_post",
"edit_book",
]
owner_group.permissions.set(
perms_model.objects.using(db_alias).filter(codename__in=perms).all()
)
# also extend these perms to admins
# This is get or create so the tests don't fail -- it should already exist
admin_group, _ = group_model.objects.using(db_alias).get_or_create(name="admin")
admin_group.permissions.add(reg_perm)
admin_group.permissions.add(admin_perm)
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0167_auto_20221125_1900"),
]
operations = [
migrations.RunPython(create_groups_and_perms, migrations.RunPython.noop)
]

View file

@ -0,0 +1,28 @@
# Generated by Django 3.2.16 on 2022-12-06 09:02
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0168_auto_20221205_1701"),
]
operations = [
migrations.AddField(
model_name="author",
name="isfdb",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
migrations.AddField(
model_name="book",
name="isfdb",
field=bookwyrm.models.fields.CharField(
blank=True, max_length=255, null=True
),
),
]

View file

@ -0,0 +1,13 @@
# Generated by Django 3.2.16 on 2022-12-11 20:00
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0168_auto_20221205_2331"),
("bookwyrm", "0169_auto_20221206_0902"),
]
operations = []

View file

@ -24,6 +24,9 @@ class Author(BookDataModel):
gutenberg_id = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
isfdb = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
# idk probably other keys would be useful here?
born = fields.DateTimeField(blank=True, null=True)
died = fields.DateTimeField(blank=True, null=True)
@ -60,6 +63,11 @@ class Author(BookDataModel):
"""generate the url from the openlibrary id"""
return f"https://openlibrary.org/authors/{self.openlibrary_key}"
@property
def isfdb_link(self):
"""generate the url from the isni id"""
return f"https://www.isfdb.org/cgi-bin/ea.cgi?{self.isfdb}"
def get_remote_id(self):
"""editions and works both use "book" instead of model_name"""
return f"https://{DOMAIN}/author/{self.id}"

View file

@ -55,6 +55,12 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
asin = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
aasin = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
isfdb = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
)
search_vector = SearchVectorField(null=True)
last_edited_by = fields.ForeignKey(
@ -73,6 +79,11 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
"""generate the url from the inventaire id"""
return f"https://inventaire.io/entity/{self.inventaire_id}"
@property
def isfdb_link(self):
"""generate the url from the isfdb id"""
return f"https://www.isfdb.org/cgi-bin/title.cgi?{self.isfdb}"
class Meta:
"""can't initialize this model, that wouldn't make sense"""

View file

@ -13,6 +13,7 @@ from django.forms import ClearableFileInput, ImageField as DjangoImageField
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.utils.encoding import filepath_to_uri
from markdown import markdown
from bookwyrm import activitypub
from bookwyrm.connectors import get_image
@ -499,6 +500,9 @@ class HtmlField(ActivitypubFieldMixin, models.TextField):
return None
return clean(value)
def field_to_activity(self, value):
return markdown(value) if value else value
class ArrayField(ActivitypubFieldMixin, DjangoArrayField):
"""activitypub-aware array field"""

View file

@ -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)

View file

@ -373,6 +373,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
"""We don't actually delete the database entry"""
# pylint: disable=attribute-defined-outside-init
self.is_active = False
self.avatar = ""
# skip the logic in this class's save()
super().save(*args, **kwargs)
@ -390,7 +391,10 @@ class User(OrderedCollectionPageMixin, AbstractUser):
self.is_active = True
self.deactivation_reason = None
self.allow_reactivation = False
super().save(broadcast=False)
super().save(
broadcast=False,
update_fields=["deactivation_reason", "is_active", "allow_reactivation"],
)
@property
def local_path(self):

View file

@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
env = Env()
env.read_env()
DOMAIN = env("DOMAIN")
VERSION = "0.5.2"
VERSION = "0.5.3"
RELEASE_API = env(
"RELEASE_API",
@ -21,7 +21,7 @@ RELEASE_API = env(
PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
JS_CACHE = "e678183c"
JS_CACHE = "ad848b97"
# email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")

View file

@ -140,6 +140,10 @@ button:focus-visible .button-invisible-overlay {
opacity: 1;
}
button.button-paragraph {
vertical-align: middle;
}
/** States
******************************************************************************/

View file

@ -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;

View file

@ -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,7 @@ $family-secondary: $family-sans-serif;
color: $grey-light !important;
}
#qrcode svg {
background-color: #a6a6a6;
}

View file

@ -48,6 +48,12 @@ let BookWyrm = new (class {
document
.querySelector("#barcode-scanner-modal")
.addEventListener("open", this.openBarcodeScanner.bind(this));
document
.querySelectorAll('form[name="register"]')
.forEach((form) =>
form.addEventListener("submit", (e) => this.setPreferredTimezone(e, form))
);
}
/**
@ -628,9 +634,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) {
@ -785,4 +791,16 @@ let BookWyrm = new (class {
initBarcodes();
}
/**
* Set preferred timezone in register form.
*
* @param {Event} event - `submit` event fired by the register form.
* @return {undefined}
*/
setPreferredTimezone(event, form) {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
form.querySelector('input[name="preferred_timezone"]').value = tz;
}
})();

View 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 %}

View file

@ -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>

View file

@ -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 %}
@ -123,16 +123,18 @@
</h2>
<p class="subtitle is-5">{% trans "Thats great!" %}</p>
<p class="title is-4 is-serif">
{% blocktrans with pages=pages_average|intcomma %}That makes an average of {{ pages }} pages per book.{% endblocktrans %}
</p>
{% if pages > 0 %}
<p class="title is-4 is-serif">
{% blocktrans with pages=pages_average|intcomma %}That makes an average of {{ pages }} pages per book.{% endblocktrans %}
</p>
{% endif %}
{% if no_page_number %}
<p class="subtitle is-6">
{% blocktrans trimmed count counter=no_page_number %}
({{ no_page_number }} book doesnt have pages)
(No page data was available for {{ no_page_number }} book)
{% plural %}
({{ no_page_number }} books dont have pages)
(No page data was available for {{ no_page_number }} books)
{% endblocktrans %}
</p>
{% endif %}

View file

@ -28,7 +28,7 @@
<meta itemprop="name" content="{{ author.name }}">
{% firstof author.aliases author.born author.died as details %}
{% firstof author.wikipedia_link author.openlibrary_key author.inventaire_id author.isni as links %}
{% firstof author.wikipedia_link author.openlibrary_key author.inventaire_id author.isni author.isfdb as links %}
{% if details or links %}
<div class="column is-3">
{% if details %}
@ -81,6 +81,14 @@
</div>
{% endif %}
{% if author.isfdb %}
<div class="mt-1">
<a itemprop="sameAs" href="{{ author.isfdb_link }}" rel="nofollow noopener noreferrer" target="_blank">
{% trans "View on ISFDB" %}
</a>
</div>
{% endif %}
{% trans "Load data" as button_text %}
{% if author.openlibrary_key %}
<div class="mt-1 is-flex">
@ -128,6 +136,14 @@
</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 %}
@ -144,7 +160,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 %}

View file

@ -101,6 +101,13 @@
{% include 'snippets/form_errors.html' with errors_list=form.goodreads_key.errors id="desc_goodreads_key" %}
</div>
<div class="field">
<label class="label" for="id_isfdb">{% trans "ISFDB:" %}</label>
{{ form.isfdb }}
{% include 'snippets/form_errors.html' with errors_list=form.isfdb.errors id="desc_isfdb" %}
</div>
<div class="field">
<label class="label" for="id_isni">{% trans "ISNI:" %}</label>
{{ form.isni }}

View file

@ -25,7 +25,7 @@
<div class="block" itemscope itemtype="https://schema.org/Book">
<div class="columns is-mobile">
<div class="column">
<h1 class="title" itemprop="name">
<h1 class="title" itemprop="name" dir="auto">
{{ book.title }}
</h1>
@ -37,7 +37,7 @@
content="{{ book.subtitle | escape }}"
>
<span class="has-text-weight-bold">
<span class="has-text-weight-bold" dir="auto">
{{ book.subtitle }}
</span>
{% endif %}
@ -52,7 +52,7 @@
{% endif %}
{% if book.authors.exists %}
<div class="subtitle">
<div class="subtitle" dir="auto">
{% trans "by" %} {% include 'snippets/authors.html' with book=book %}
</div>
{% endif %}
@ -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>
@ -158,6 +158,13 @@
{% endif %}
</p>
{% endif %}
{% if book.isfdb %}
<p>
<a href="{{ book.isfdb_link }}" target="_blank" rel="nofollow noopener noreferrer">
{% trans "View on ISFDB" %}
</a>
</p>
{% endif %}
</section>
</div>
@ -189,15 +196,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 +238,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>

View file

@ -1,7 +1,7 @@
{% spaceless %}
{% load i18n %}
{% if book.isbn_13 or book.oclc_number or book.asin %}
{% if book.isbn_13 or book.oclc_number or book.asin or book.aasin or book.isfdb %}
<dl>
{% if book.isbn_13 %}
<div class="is-flex">
@ -23,6 +23,27 @@
<dd>{{ book.asin }}</dd>
</div>
{% endif %}
{% if book.aasin %}
<div class="is-flex">
<dt class="mr-1">{% trans "Audible ASIN:" %}</dt>
<dd>{{ book.aasin }}</dd>
</div>
{% endif %}
{% if book.isfdb %}
<div class="is-flex">
<dt class="mr-1">{% trans "ISFDB ID:" %}</dt>
<dd>{{ book.isfdb }}</dd>
</div>
{% endif %}
{% if book.goodreads_key %}
<div class="is-flex">
<dt class="mr-1">{% trans "Goodreads:" %}</dt>
<dd>{{ book.goodreads_key }}</dd>
</div>
{% endif %}
</dl>
{% endif %}
{% endspaceless %}

View file

@ -65,17 +65,17 @@
<input type="hidden" name="author-match-count" value="{{ author_matches|length }}">
<div class="column is-half">
{% for author in author_matches %}
<fieldset>
<fieldset class="block">
<legend class="title is-5 mb-1">
{% blocktrans with name=author.name %}Is "{{ name }}" one of these authors?{% endblocktrans %}
</legend>
{% with forloop.counter0 as counter %}
{% for match in author.matches %}
<label class="label">
<label class="label mb-0">
<input type="radio" name="author_match-{{ counter }}" value="{{ match.id }}" required>
{{ match.name }}
</label>
<p class="help ml-5 mb-2">
<p class="help ml-5 mb-0 mt-0">
{% with book_title=match.book_set.first.title alt_title=match.bio %}
{% if book_title %}
<a href="{{ match.local_path }}" target="_blank" rel="nofollow noopener noreferrer">{% blocktrans trimmed %}
@ -98,6 +98,9 @@
</label>
{% endwith %}
</fieldset>
{% if not forloop.last %}
<hr aria-hidden="true">
{% endif %}
{% endfor %}
</div>
{% else %}

View file

@ -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>
@ -327,6 +327,15 @@
{% include 'snippets/form_errors.html' with errors_list=form.inventaire_id.errors id="desc_inventaire_id" %}
</div>
<div class="field">
<label class="label" for="id_goodreads_key">
{% trans "Goodreads key:" %}
</label>
{{ form.goodreads_key }}
{% include 'snippets/form_errors.html' with errors_list=form.goodreads_key.errors id="desc_goodreads_key" %}
</div>
<div class="field">
<label class="label" for="id_oclc_number">
{% trans "OCLC Number:" %}
@ -344,6 +353,24 @@
{% include 'snippets/form_errors.html' with errors_list=form.ASIN.errors id="desc_ASIN" %}
</div>
<div class="field">
<label class="label" for="id_aasin">
{% trans "Audible ASIN:" %}
</label>
{{ form.aasin }}
{% include 'snippets/form_errors.html' with errors_list=form.AASIN.errors id="desc_AASIN" %}
</div>
<div class="field">
<label class="label" for="id_isfdb">
{% trans "ISFDB ID:" %}
</label>
{{ form.isfdb }}
{% include 'snippets/form_errors.html' with errors_list=form.isfdb.errors id="desc_isfdb" %}
</div>
</div>
</section>
</div>

View file

@ -0,0 +1,12 @@
{% extends 'email/html_layout.html' %}
{% load i18n %}
{% block content %}
<p>
{% blocktrans trimmed %}
This is a test email.
{% endblocktrans %}
</p>
{% endblock %}

View file

@ -0,0 +1,4 @@
{% load i18n %}
{% blocktrans trimmed %}
Test email
{% endblocktrans %}

View file

@ -0,0 +1,9 @@
{% extends 'email/text_layout.html' %}
{% load i18n %}
{% block content %}
{% blocktrans trimmed %}
This is a test email.
{% endblocktrans %}
{% endblock %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -13,6 +13,7 @@
<link rel="search" type="application/opensearchdescription+xml" href="{% url 'opensearch' %}" title="{% blocktrans with site_name=site.name %}{{ site_name }} search{% endblocktrans %}" />
<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 %}">
{% if preview_images_enabled is True %}
<meta name="twitter:card" content="summary_large_image">

View file

@ -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>

View file

@ -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 %}

View file

@ -65,7 +65,7 @@
{# This happens if the list item was deleted #}
{% blocktrans trimmed %}
<a href="{{ related_user_link }}">{{ related_user }}</a>
added added a book to one of your lists
added a book to one of your lists
{% endblocktrans %}
{% elif related_list.curation != "curated" %}

View file

@ -44,7 +44,7 @@
{% 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">
<section class="column">
<figure class="m-4" id="qrcode">{{ qrcode | safe }}</figure>
<details class="details-panel box">
<summary>

View file

@ -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 %}

View file

@ -58,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 %}

View file

@ -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 }})

View file

@ -0,0 +1,96 @@
{% extends 'settings/layout.html' %}
{% load humanize %}
{% load i18n %}
{% load celery_tags %}
{% block title %}{% trans "Email Configuration" %}{% endblock %}
{% block header %}{% trans "Email Configuration" %}{% endblock %}
{% block panel %}
{% if error %}
<div class="notification is-danger is-light">
<span class="icon icon-x" aria-hidden="true"></span>
<span>
{% trans "Error sending test email:" %}
{{ error }}
</span>
</div>
{% elif success %}
<div class="notification is-success is-light">
<span class="icon icon-check" aria-hidden="true"></span>
<span>
{% trans "Successfully sent test email." %}
</span>
</div>
{% endif %}
<section class="block content">
<dl>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Email sender:" %}
</dt>
<dd>
{{ email_sender }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Email backend:" %}
</dt>
<dd>
<code>{{ email_backend }}</code>
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Host:" %}
</dt>
<dd>
<code>{{ email_host }}</code>
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Host user:" %}
</dt>
<dd>
<code>{% firstof email_host_user "-" %}</code>
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Port:" %}
</dt>
<dd>
<code>{{ email_port }}</code>
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Use TLS:" %}
</dt>
<dd>
{{ email_use_tls|yesno }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Use SSL:" %}
</dt>
<dd>
{{ email_use_ssl|yesno }}
</dd>
</dl>
</section>
<section class="block content box">
<p>
{% blocktrans trimmed with email=request.user.email %}
Send test email to {{ email }}
{% endblocktrans %}
</p>
<form action="{% url 'settings-email-config' %}" method="post">
{% csrf_token %}
<button type="submit" class="button is-success">
{% trans "Send test email" %}
</button>
</form>
</section>
{% endblock %}

View file

@ -81,12 +81,14 @@
{% url 'settings-imports' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Imports" %}</a>
</li>
</ul>
<ul class="menu-list">
<li>
{% url 'settings-celery' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Celery status" %}</a>
</li>
<li>
{% url 'settings-email-config' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Email Configuration" %}</a>
</li>
</ul>
{% endif %}
{% if perms.bookwyrm.edit_instance_settings %}
@ -101,10 +103,21 @@
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Site Settings" %}</a>
{% block site-subtabs %}{% endblock %}
</li>
<li>
{% if perms.bookwyrm.manage_registration %}
{% url 'settings-registration' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Registration" %}</a>
{% else %}
{% url 'settings-registration-limited' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Registration" %}</a>
{% endif %}
</li>
{% if perms.bookwyrm.system_administration %}
<li>
{% url 'settings-themes' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Themes" %}</a>
</li>
{% endif %}
</ul>
{% endif %}
</nav>

View file

@ -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>

View file

@ -0,0 +1,83 @@
{% extends 'settings/layout.html' %}
{% load i18n %}
{% block title %}{% trans "Registration" %}{% endblock %}
{% block header %}{% trans "Registration" %}{% endblock %}
{% block panel %}
{% if success %}
<div class="notification is-success is-light">
<span class="icon icon-check" aria-hidden="true"></span>
<span>
{% trans "Settings saved" %}
</span>
</div>
{% endif %}
{% if form.errors %}
<div class="notification is-danger is-light">
<span class="icon icon-x" aria-hidden="true"></span>
<span>
{% trans "Unable to save settings" %}
</span>
</div>
{% endif %}
<form
action="{% url 'settings-registration' %}"
method="POST"
class="content"
enctype="multipart/form-data"
>
{% csrf_token %}
<section class="block box" id="registration">
<div class="field">
<label class="label" for="id_allow_registration">
{{ form.allow_registration }}
{% trans "Allow registration" %}
</label>
</div>
<div class="field">
<label class="label mb-0" for="id_require_confirm_email">
{{ form.require_confirm_email }}
{% trans "Require users to confirm email address" %}
</label>
<p class="help" id="desc_require_confirm_email">{% trans "(Recommended if registration is open)" %}</p>
</div>
<div class="field">
<label class="label" for="id_allow_invite_requests">
{{ form.allow_invite_requests }}
{% trans "Allow invite requests" %}
</label>
</div>
<div class="field">
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
{{ form.invite_request_text }}
{% 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">
{{ form.invite_request_question }}
{% trans "Set a question for invite requests" %}
</label>
</div>
<div class="field">
<label class="label" for="id_invite_question_text">
{% trans "Question:" %}
{{ form.invite_question_text }}
</label>
</div>
<div class="field">
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
{{ form.registration_closed_text }}
</div>
</section>
<footer class="block">
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
</footer>
</form>
{% endblock %}

View file

@ -0,0 +1,81 @@
{% extends 'settings/layout.html' %}
{% load i18n %}
{% block title %}{% trans "Registration" %}{% endblock %}
{% block header %}{% trans "Registration" %}{% endblock %}
{% block panel %}
{% if success %}
<div class="notification is-success is-light">
<span class="icon icon-check" aria-hidden="true"></span>
<span>
{% trans "Settings saved" %}
</span>
</div>
{% endif %}
{% if form.errors %}
<div class="notification is-danger is-light">
<span class="icon icon-x" aria-hidden="true"></span>
<span>
{% trans "Unable to save settings" %}
</span>
</div>
{% endif %}
{% if site.allow_registration %}
<div class="notification">
{% trans "Registration is enabled on this instance" %}
</div>
{% else %}
<form
action="{% url 'settings-registration-limited' %}"
method="POST"
class="content"
enctype="multipart/form-data"
>
{% csrf_token %}
<section class="block box" id="registration">
{% if site.allow_invite_requests %}
<div class="field">
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
{{ form.invite_request_text }}
{% 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">
{{ form.invite_request_question }}
{% trans "Set a question for invite requests" %}
</label>
</div>
<div class="field">
<label class="label" for="id_invite_question_text">
{% trans "Question:" %}
{{ form.invite_question_text }}
</label>
</div>
{% else %}
<input type="hidden" name="invite_request_text" value="{{ form.invite_request_text.value }}">
<input type="hidden" name="invite_request_question" value="{{ form.invite_request_question.value }}">
<input type="hidden" name="invite_question_text" value="{{ form.invite_question_text.value }}">
{% endif %}
{% if not site.allow_invite_requests and not site.allow_registration %}
<div class="field">
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
{{ form.registration_closed_text }}
</div>
{% else %}
<input type="hidden" name="registration_closed_text" value="{{ form.registration_closed_text.value }}">
{% endif %}
</section>
<footer class="block">
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
</footer>
</form>
{% endif %}
{% endblock %}

View file

@ -10,7 +10,6 @@
<li><a href="#instance-info">{% trans "Instance Info" %}</a></li>
<li><a href="#display">{% trans "Display" %}</a></li>
<li><a href="#footer">{% trans "Footer Content" %}</a></li>
<li><a href="#registration">{% trans "Registration" %}</a></li>
</ul>
{% endblock %}
@ -68,6 +67,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>
@ -128,55 +140,6 @@
</div>
</section>
<hr aria-hidden="true">
<section class="block" id="registration">
<h2 class="title is-4">{% trans "Registration" %}</h2>
<div class="box">
<div class="field">
<label class="label" for="id_allow_registration">
{{ site_form.allow_registration }}
{% trans "Allow registration" %}
</label>
</div>
<div class="field">
<label class="label mb-0" for="id_require_confirm_email">
{{ site_form.require_confirm_email }}
{% trans "Require users to confirm email address" %}
</label>
<p class="help" id="desc_require_confirm_email">{% trans "(Recommended if registration is open)" %}</p>
</div>
<div class="field">
<label class="label" for="id_allow_invite_requests">
{{ site_form.allow_invite_requests }}
{% trans "Allow invite requests" %}
</label>
</div>
<div class="field">
<label class="label" for="id_invite_requests_question">
{{ site_form.invite_request_question }}
{% trans "Set a question for invite requests" %}
</label>
</div>
<div class="field">
<label class="label" for="id_invite_question_text">
{% trans "Question:" %}
{{ site_form.invite_question_text }}
</label>
</div>
<div class="field">
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
{{ site_form.registration_closed_text }}
</div>
<div class="field">
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
{{ site_form.invite_request_text }}
{% include 'snippets/form_errors.html' with errors_list=site_form.invite_request_text.errors id="desc_invite_request_text" %}
</div>
</div>
</section>
<footer class="block">
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
</footer>

View file

@ -10,7 +10,7 @@
{% csrf_token %}
<p>
{% blocktrans trimmed with username=user.localname %}
Are you sure you want to delete <strong>{{ username}}</strong>'s account? This action cannot be undone. To proceed, please enter your password to confirm deletion.
Are you sure you want to delete <strong>{{username}}</strong>'s account? This action cannot be undone. To proceed, please enter your password to confirm deletion.
{% endblocktrans %}
</p>
<div class="field">

View file

@ -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>

View file

@ -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>

View file

@ -29,6 +29,11 @@
<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 %}

View file

@ -58,6 +58,8 @@
</div>
</div>
<input type="hidden" name="preferred_timezone" />
<div class="field">
<div class="control">
<button class="button is-primary" type="submit">

View file

@ -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 %}

View file

@ -66,6 +66,10 @@
<li{% if url == request.path or url == request.path|add:'/' %} class="is-active"{% endif %}>
<a href="{{ url }}">{% trans "Activity" %}</a>
</li>
{% url 'user-reviews-comments' user|username as url %}
<li{% if url == request.path or url == request.path|add:'/' %} class="is-active"{% endif %}>
<a href="{{ url }}">{% trans "Reviews and Comments" %}</a>
</li>
{% if is_self or user.goal.exists %}
{% now 'Y' as year %}
{% url 'user-goal' user|username year as url %}

View file

@ -0,0 +1,30 @@
{% extends 'user/layout.html' %}
{% load i18n %}
{% load utilities %}
{% block title %}{{ user.display_name }}{% endblock %}
{% block header %}
<div class="columns is-mobile">
<div class="column">
<h1 class="title">{% trans "Reviews and Comments" %}</h1>
</div>
</div>
{% endblock %}
{% block panel %}
<div>
{% for activity in activities %}
<div class="block" id="feed_{{ activity.id }}">
{% include 'snippets/status/status.html' with status=activity %}
</div>
{% endfor %}
{% if not activities %}
<div class="block">
<p>{% trans "No reviews or comments yet!" %}</p>
</div>
{% endif %}
{% include 'snippets/pagination.html' with page=activities path=path %}
</div>
{% endblock %}

View file

@ -21,6 +21,7 @@
"openlibrary_key": "OL29486417M",
"librarything_key": null,
"goodreads_key": null,
"isfdb": null,
"attachment": [
{
"url": "https://bookwyrm.social/images/covers/50202953._SX318_.jpg",

View file

@ -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

1 Title Authors Contributors ISBN 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?
2 Always Coming Home Ursula K. Le Guin, Todd Barton, Margaret Chodos-Irvine 9780520227354 to-read 2021/05/10 0 No
3 Subprime Attention Crisis Tim Hwang 0374538654 read 2021/05/10 1 informative fast 5.0 No

View file

@ -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"""

View file

@ -12,7 +12,7 @@ class InitDB(TestCase):
def test_init_groups(self):
"""Create groups"""
initdb.init_groups()
self.assertEqual(Group.objects.count(), 3)
self.assertEqual(Group.objects.count(), 4)
self.assertTrue(Group.objects.filter(name="admin").exists())
self.assertTrue(Group.objects.filter(name="moderator").exists())
self.assertTrue(Group.objects.filter(name="editor").exists())
@ -87,7 +87,7 @@ class InitDB(TestCase):
command.handle()
# everything should have been called
self.assertEqual(Group.objects.count(), 3)
self.assertEqual(Group.objects.count(), 4)
self.assertTrue(Permission.objects.exists())
self.assertEqual(models.Connector.objects.count(), 3)
self.assertEqual(models.SiteSettings.objects.count(), 1)
@ -99,7 +99,7 @@ class InitDB(TestCase):
command.handle(limit="group")
# everything should have been called
self.assertEqual(Group.objects.count(), 3)
self.assertEqual(Group.objects.count(), 4)
self.assertEqual(models.Connector.objects.count(), 0)
self.assertEqual(models.SiteSettings.objects.count(), 0)
self.assertEqual(models.LinkDomain.objects.count(), 0)

View file

@ -383,16 +383,16 @@ class ActivitypubMixins(TestCase):
self.assertEqual(page_1.partOf, "http://fish.com/")
self.assertEqual(page_1.id, "http://fish.com/?page=1")
self.assertEqual(page_1.next, "http://fish.com/?page=2")
self.assertEqual(page_1.orderedItems[0]["content"], "test status 29")
self.assertEqual(page_1.orderedItems[1]["content"], "test status 28")
self.assertEqual(page_1.orderedItems[0]["content"], "<p>test status 29</p>")
self.assertEqual(page_1.orderedItems[1]["content"], "<p>test status 28</p>")
page_2 = to_ordered_collection_page(
models.Status.objects.all(), "http://fish.com/", page=2
)
self.assertEqual(page_2.partOf, "http://fish.com/")
self.assertEqual(page_2.id, "http://fish.com/?page=2")
self.assertEqual(page_2.orderedItems[0]["content"], "test status 14")
self.assertEqual(page_2.orderedItems[-1]["content"], "test status 0")
self.assertEqual(page_2.orderedItems[0]["content"], "<p>test status 14</p>")
self.assertEqual(page_2.orderedItems[-1]["content"], "<p>test status 0</p>")
def test_to_ordered_collection(self, *_):
"""convert a queryset into an ordered collection object"""
@ -420,8 +420,8 @@ class ActivitypubMixins(TestCase):
)
self.assertEqual(page_2.partOf, "http://fish.com/")
self.assertEqual(page_2.id, "http://fish.com/?page=2")
self.assertEqual(page_2.orderedItems[0]["content"], "test status 14")
self.assertEqual(page_2.orderedItems[-1]["content"], "test status 0")
self.assertEqual(page_2.orderedItems[0]["content"], "<p>test status 14</p>")
self.assertEqual(page_2.orderedItems[-1]["content"], "<p>test status 0</p>")
def test_broadcast_task(self, *_):
"""Should be calling asyncio"""

View file

@ -132,7 +132,7 @@ class Status(TestCase):
activity = status.to_activity()
self.assertEqual(activity["id"], status.remote_id)
self.assertEqual(activity["type"], "Note")
self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["content"], "<p>test content</p>")
self.assertEqual(activity["sensitive"], False)
def test_status_to_activity_tombstone(self, *_):
@ -156,7 +156,7 @@ class Status(TestCase):
activity = status.to_activity(pure=True)
self.assertEqual(activity["id"], status.remote_id)
self.assertEqual(activity["type"], "Note")
self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["content"], "<p>test content</p>")
self.assertEqual(activity["sensitive"], False)
self.assertEqual(activity["attachment"], [])
@ -170,7 +170,7 @@ class Status(TestCase):
activity = status.to_activity()
self.assertEqual(activity["id"], status.remote_id)
self.assertEqual(activity["type"], "GeneratedNote")
self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["content"], "<p>test content</p>")
self.assertEqual(activity["sensitive"], False)
self.assertEqual(len(activity["tag"]), 2)
@ -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,
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_comment_to_activity(self, *_):
"""subclass of the base model version with a "pure" serializer"""
@ -208,7 +208,7 @@ class Status(TestCase):
activity = status.to_activity()
self.assertEqual(activity["id"], status.remote_id)
self.assertEqual(activity["type"], "Comment")
self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["content"], "<p>test content</p>")
self.assertEqual(activity["inReplyToBook"], self.book.remote_id)
def test_comment_to_pure_activity(self, *_):
@ -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"""
@ -243,8 +243,8 @@ class Status(TestCase):
activity = status.to_activity()
self.assertEqual(activity["id"], status.remote_id)
self.assertEqual(activity["type"], "Quotation")
self.assertEqual(activity["quote"], "a sickening sense")
self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["quote"], "<p>a sickening sense</p>")
self.assertEqual(activity["content"], "<p>test content</p>")
self.assertEqual(activity["inReplyToBook"], self.book.remote_id)
def test_quotation_to_pure_activity(self, *_):
@ -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"""
@ -285,7 +285,7 @@ class Status(TestCase):
self.assertEqual(activity["type"], "Review")
self.assertEqual(activity["rating"], 3)
self.assertEqual(activity["name"], "Review name")
self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["content"], "<p>test content</p>")
self.assertEqual(activity["inReplyToBook"], self.book.remote_id)
def test_review_to_pure_activity(self, *_):
@ -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"""

View file

@ -0,0 +1,46 @@
""" test for app action functionality """
from unittest.mock import patch
from django.contrib.auth.models import Group
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.management.commands import initdb
from bookwyrm.tests.validate_html import validate_html
class EmailConfigViews(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()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
self.local_user = models.User.objects.create_user(
"mouse@local.com",
"mouse@mouse.mouse",
"password",
local=True,
localname="mouse",
)
initdb.init_groups()
initdb.init_permissions()
group = Group.objects.get(name="admin")
self.local_user.groups.set([group])
models.SiteSettings.objects.create()
def test_email_config_get(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.EmailConfig.as_view()
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)

View file

@ -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

View file

@ -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()

View file

@ -58,6 +58,7 @@ class RegisterViews(TestCase):
"localname": "nutria-user.user_nutria",
"password": "mouseword",
"email": "aa@bb.cccc",
"preferred_timezone": "Europe/Berlin",
},
)
with patch("bookwyrm.views.landing.register.login"):
@ -68,6 +69,7 @@ class RegisterViews(TestCase):
self.assertEqual(nutria.username, f"nutria-user.user_nutria@{DOMAIN}")
self.assertEqual(nutria.localname, "nutria-user.user_nutria")
self.assertEqual(nutria.local, True)
self.assertEqual(nutria.preferred_timezone, "Europe/Berlin")
@patch("bookwyrm.emailing.send_email.delay")
def test_register_email_confirm(self, *_):
@ -198,6 +200,58 @@ class RegisterViews(TestCase):
self.assertEqual(models.User.objects.count(), 1)
validate_html(response.render())
def test_register_default_preferred_timezone(self, *_):
"""invalid preferred timezone strings should just default to UTC"""
view = views.Register.as_view()
self.assertEqual(models.User.objects.count(), 1)
request = self.factory.post(
"register/",
{
"localname": "nutria1",
"password": "mouseword",
"email": "aa1@bb.cccc",
"preferred_timezone": "invalid-tz",
},
)
with patch("bookwyrm.views.landing.register.login"):
response = view(request)
self.assertEqual(response.status_code, 302)
self.assertEqual(models.User.objects.count(), 2)
nutria = models.User.objects.last()
self.assertEqual(nutria.preferred_timezone, "UTC")
request = self.factory.post(
"register/",
{
"localname": "nutria2",
"password": "mouseword",
"email": "aa2@bb.cccc",
"preferred_timezone": "",
},
)
with patch("bookwyrm.views.landing.register.login"):
response = view(request)
self.assertEqual(response.status_code, 302)
self.assertEqual(models.User.objects.count(), 3)
nutria = models.User.objects.last()
self.assertEqual(nutria.preferred_timezone, "UTC")
request = self.factory.post(
"register/",
{
"localname": "nutria3",
"password": "mouseword",
"email": "aa3@bb.cccc",
},
)
with patch("bookwyrm.views.landing.register.login"):
response = view(request)
self.assertEqual(response.status_code, 302)
self.assertEqual(models.User.objects.count(), 4)
nutria = models.User.objects.last()
self.assertEqual(nutria.preferred_timezone, "UTC")
def test_register_closed_instance(self, *_):
"""you can't just register"""
view = views.Register.as_view()

View file

@ -63,7 +63,7 @@ class ExportViews(TestCase):
# pylint: disable=line-too-long
self.assertEqual(
result[0],
b"title,author_text,remote_id,openlibrary_key,inventaire_id,librarything_key,goodreads_key,bnf_id,viaf,wikidata,asin,isbn_10,isbn_13,oclc_number,rating,review_name,review_cw,review_content\r\n",
b"title,author_text,remote_id,openlibrary_key,inventaire_id,librarything_key,goodreads_key,bnf_id,viaf,wikidata,asin,aasin,isfdb,isbn_10,isbn_13,oclc_number,rating,review_name,review_cw,review_content\r\n",
)
expected = f"Test Book,,{self.book.remote_id},,,,,beep,,,,123456789X,9781234567890,,,,,\r\n"
expected = f"Test Book,,{self.book.remote_id},,,,,beep,,,,,,123456789X,9781234567890,,,,,\r\n"
self.assertEqual(result[1].decode("utf-8"), expected)

View file

@ -233,3 +233,19 @@ class UserViews(TestCase):
result = views.user_redirect(request, "mouse")
self.assertEqual(result.status_code, 302)
def test_reviews_comments_page(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.UserReviewsComments.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request, "mouse")
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
request.user = self.anonymous_user
result = view(request, "mouse")
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)

View file

@ -86,6 +86,16 @@ urlpatterns = [
r"^settings/dashboard/?$", views.Dashboard.as_view(), name="settings-dashboard"
),
re_path(r"^settings/site-settings/?$", views.Site.as_view(), name="settings-site"),
re_path(
r"^settings/site-registration/?$",
views.RegistrationLimited.as_view(),
name="settings-registration-limited",
),
re_path(
r"^settings/site-registration-admin/?$",
views.Registration.as_view(),
name="settings-registration",
),
re_path(r"^settings/themes/?$", views.Themes.as_view(), name="settings-themes"),
re_path(
r"^settings/themes/(?P<theme_id>\d+)/delete/?$",
@ -119,7 +129,7 @@ urlpatterns = [
),
re_path(
r"^settings/email-preview/?$",
views.admin.site.email_preview,
views.admin.email_config.email_preview,
name="settings-email-preview",
),
re_path(
@ -319,10 +329,16 @@ urlpatterns = [
re_path(
r"^settings/celery/?$", views.CeleryStatus.as_view(), name="settings-celery"
),
re_path(
r"^settings/email-config/?$",
views.EmailConfig.as_view(),
name="settings-email-config",
),
# landing pages
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"),
@ -414,6 +430,11 @@ urlpatterns = [
name="user-relationships",
),
re_path(r"^hide-suggestions/?$", views.hide_suggestions, name="hide-suggestions"),
re_path(
rf"{USER_PATH}/reviews-comments",
views.UserReviewsComments.as_view(),
name="user-reviews-comments",
),
# groups
re_path(rf"{USER_PATH}/groups/?$", views.UserGroups.as_view(), name="user-groups"),
re_path(

View file

@ -10,6 +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.email_config import EmailConfig
from .admin.imports import (
ImportList,
disable_imports,
@ -28,7 +29,7 @@ from .admin.reports import (
unsuspend_user,
moderator_delete_user,
)
from .admin.site import Site
from .admin.site import Site, Registration, RegistrationLimited
from .admin.themes import Themes, delete_theme
from .admin.user_admin import UserAdmin, UserAdminList
@ -65,7 +66,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
@ -142,7 +143,13 @@ from .setup import InstanceConfig, CreateAdmin
from .status import CreateStatus, EditStatus, DeleteStatus, update_progress
from .status import edit_readthrough
from .updates import get_notification_count, get_unread_status_string
from .user import User, hide_suggestions, user_redirect, toggle_guided_tour
from .user import (
User,
UserReviewsComments,
hide_suggestions,
user_redirect,
toggle_guided_tour,
)
from .relationships import Relationships
from .wellknown import *
from .annual_summary import (

View file

@ -0,0 +1,65 @@
""" is your email running? """
from django.contrib.auth.decorators import login_required, permission_required
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import emailing
from bookwyrm import settings
# pylint: disable= no-self-use
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
name="dispatch",
)
class EmailConfig(View):
"""View and test your emailing setup"""
def get(self, request):
"""View email config"""
data = view_data()
# TODO: show email previews
return TemplateResponse(request, "settings/email_config.html", data)
def post(self, request):
"""Send test email"""
data = view_data()
try:
emailing.test_email(request.user)
data["success"] = True
except Exception as err: # pylint: disable=broad-except
data["error"] = err
return TemplateResponse(request, "settings/email_config.html", data)
def view_data():
"""helper to get data for view"""
return {
"email_backend": settings.EMAIL_BACKEND,
"email_host": settings.EMAIL_HOST,
"email_port": settings.EMAIL_PORT,
"Email_host_user": settings.EMAIL_HOST_USER,
"email_use_tls": settings.EMAIL_USE_TLS,
"email_use_ssl": settings.EMAIL_USE_SSL,
"email_sender": settings.EMAIL_SENDER,
}
@login_required
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
def email_preview(request):
"""for development, renders and example email template"""
template = request.GET.get("email")
data = emailing.email_data()
data["subject_path"] = f"email/{template}/subject.html"
data["html_content_path"] = f"email/{template}/html_content.html"
data["text_content_path"] = f"email/{template}/text_content.html"
data["reset_link"] = "https://example.com/link"
data["invite_link"] = "https://example.com/link"
data["confirmation_link"] = "https://example.com/link"
data["confirmation_code"] = "AKJHKDGKJSDFG"
data["reporter"] = "ConcernedUser"
data["reportee"] = "UserName"
data["report_link"] = "https://example.com/link"
return TemplateResponse(request, "email/preview.html", data)

View file

@ -4,7 +4,7 @@ from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import emailing, forms, models
from bookwyrm import forms, models
# pylint: disable= no-self-use
@ -35,20 +35,55 @@ class Site(View):
return TemplateResponse(request, "settings/site.html", data)
@login_required
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
def email_preview(request):
"""for development, renders and example email template"""
template = request.GET.get("email")
data = emailing.email_data()
data["subject_path"] = f"email/{template}/subject.html"
data["html_content_path"] = f"email/{template}/html_content.html"
data["text_content_path"] = f"email/{template}/text_content.html"
data["reset_link"] = "https://example.com/link"
data["invite_link"] = "https://example.com/link"
data["confirmation_link"] = "https://example.com/link"
data["confirmation_code"] = "AKJHKDGKJSDFG"
data["reporter"] = "ConcernedUser"
data["reportee"] = "UserName"
data["report_link"] = "https://example.com/link"
return TemplateResponse(request, "email/preview.html", data)
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
name="dispatch",
)
class RegistrationLimited(View):
"""Things related to registering that non-admins owners can change"""
def get(self, request):
"""edit form"""
site = models.SiteSettings.objects.get()
data = {"form": forms.RegistrationLimitedForm(instance=site)}
return TemplateResponse(request, "settings/registration_limited.html", data)
def post(self, request):
"""edit the site settings"""
site = models.SiteSettings.objects.get()
form = forms.RegistrationLimitedForm(request.POST, request.FILES, instance=site)
if not form.is_valid():
data = {"form": form}
return TemplateResponse(request, "settings/registration_limited.html", data)
site = form.save(request)
data = {"form": forms.RegistrationLimitedForm(instance=site), "success": True}
return TemplateResponse(request, "settings/registration_limited.html", data)
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.manage_registration", raise_exception=True),
name="dispatch",
)
class Registration(View):
"""Control everything about registration"""
def get(self, request):
"""edit form"""
site = models.SiteSettings.objects.get()
data = {"form": forms.RegistrationForm(instance=site)}
return TemplateResponse(request, "settings/registration.html", data)
def post(self, request):
"""edit the site settings"""
site = models.SiteSettings.objects.get()
form = forms.RegistrationForm(request.POST, request.FILES, instance=site)
if not form.is_valid():
data = {"form": form}
return TemplateResponse(request, "settings/registration.html", data)
site = form.save(request)
data = {"form": forms.RegistrationForm(instance=site), "success": True}
return TemplateResponse(request, "settings/registration.html", data)

View file

@ -12,7 +12,7 @@ from bookwyrm import forms, models
# pylint: disable= no-self-use
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
permission_required("bookwyrm.system_administration", raise_exception=True),
name="dispatch",
)
class Themes(View):
@ -46,7 +46,7 @@ def get_view_data():
@require_POST
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
@permission_required("bookwyrm.system_administration", raise_exception=True)
# pylint: disable=unused-argument
def delete_theme(request, theme_id):
"""Remove a theme"""

View file

@ -49,6 +49,8 @@ class Editions(View):
"isbn_13",
"oclc_number",
"asin",
"aasin",
"isfdb",
]
search_filter_entries = [
{f"{f}__icontains": query} for f in searchable_fields

View file

@ -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")

View file

@ -1,4 +1,5 @@
""" class views for login/register views """
import pytz
from django.contrib.auth import login
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, redirect
@ -55,6 +56,10 @@ class Register(View):
localname = form.data["localname"].strip()
email = form.data["email"]
password = form.data["password"]
try:
preferred_timezone = pytz.timezone(form.data.get("preferred_timezone"))
except pytz.exceptions.UnknownTimeZoneError:
preferred_timezone = pytz.utc
# make sure the email isn't blocked as spam
email_domain = email.split("@")[-1]
@ -71,6 +76,7 @@ class Register(View):
local=True,
deactivation_reason="pending" if settings.require_confirm_email else None,
is_active=not settings.require_confirm_email,
preferred_timezone=preferred_timezone,
)
if invite:
invite.times_used += 1
@ -105,9 +111,7 @@ class ConfirmEmailCode(View):
request, "confirm_email/confirm_email.html", {"valid": False}
)
# update the user
user.is_active = True
user.deactivation_reason = None
user.save(broadcast=False, update_fields=["is_active", "deactivation_reason"])
user.reactivate()
# direct the user to log in
return redirect("login", confirmed="confirmed")

View file

@ -1,6 +1,7 @@
""" The user profile """
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Q
from django.http import Http404
from django.shortcuts import redirect
from django.template.response import TemplateResponse
@ -100,6 +101,49 @@ class User(View):
return TemplateResponse(request, "user/user.html", data)
class UserReviewsComments(View):
"""user's activity filtered by reviews and comments"""
def get(self, request, username):
"""user's activity filtered by reviews and comments"""
user = get_user_from_username(request.user, username)
is_self = request.user.id == user.id
activities = (
models.Status.privacy_filter(
request.user,
)
.filter(
Q(review__isnull=False) | Q(comment__isnull=False),
user=user,
)
.exclude(
privacy="direct",
)
.select_related(
"user",
"reply_parent",
"review__book",
"comment__book",
"quotation__book",
)
.prefetch_related(
"mention_books",
"mention_users",
"attachments",
)
)
paginated = Paginator(activities, PAGE_LENGTH)
data = {
"user": user,
"is_self": is_self,
"activities": paginated.get_page(request.GET.get("page", 1)),
}
return TemplateResponse(request, "user/reviews_comments.html", data)
@require_POST
@login_required
def hide_suggestions(request):