mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2025-04-13 22:04:05 +00:00
Merge branch 'main' into production
This commit is contained in:
commit
dfba63f977
83 changed files with 5345 additions and 2145 deletions
|
@ -1,3 +0,0 @@
|
|||
@charset "utf-8";
|
||||
|
||||
// Copy this file to bookwyrm/static/css/ and set your instance custom styles.
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -17,7 +17,8 @@
|
|||
.env
|
||||
/images/
|
||||
bookwyrm/static/css/bookwyrm.css
|
||||
bookwyrm/static/css/_instance-settings.scss
|
||||
bookwyrm/static/css/themes/
|
||||
!bookwyrm/static/css/themes/bookwyrm-*.scss
|
||||
|
||||
# Testing
|
||||
.coverage
|
||||
|
|
|
@ -13,7 +13,7 @@ Social reading and reviewing, decentralized with ActivityPub
|
|||
- [Set up Bookwyrm](#set-up-bookwyrm)
|
||||
|
||||
## Joining BookWyrm
|
||||
BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://docs.joinbookwyrm.com/instances.html) list.
|
||||
BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://joinbookwyrm.com/instances/) list.
|
||||
|
||||
You can request an invite by entering your email address at https://bookwyrm.social.
|
||||
|
||||
|
|
|
@ -39,4 +39,5 @@ class Person(ActivityObject):
|
|||
bookwyrmUser: bool = False
|
||||
manuallyApprovesFollowers: str = False
|
||||
discoverable: str = False
|
||||
hideFollows: str = False
|
||||
type: str = "Person"
|
||||
|
|
|
@ -8,8 +8,20 @@ def site_settings(request): # pylint: disable=unused-argument
|
|||
if not request.is_secure():
|
||||
request_protocol = "http://"
|
||||
|
||||
site = models.SiteSettings.objects.get()
|
||||
theme = "css/themes/bookwyrm-light.scss"
|
||||
if (
|
||||
hasattr(request, "user")
|
||||
and request.user.is_authenticated
|
||||
and request.user.theme
|
||||
):
|
||||
theme = request.user.theme.path
|
||||
elif site.default_theme:
|
||||
theme = site.default_theme.path
|
||||
|
||||
return {
|
||||
"site": models.SiteSettings.objects.get(),
|
||||
"site": site,
|
||||
"site_theme": theme,
|
||||
"active_announcements": models.Announcement.active_announcements(),
|
||||
"thumbnail_generation_enabled": settings.ENABLE_THUMBNAIL_GENERATION,
|
||||
"media_full_url": settings.MEDIA_FULL_URL,
|
||||
|
|
|
@ -153,8 +153,10 @@ class EditUserForm(CustomForm):
|
|||
"manually_approves_followers",
|
||||
"default_post_privacy",
|
||||
"discoverable",
|
||||
"hide_follows",
|
||||
"preferred_timezone",
|
||||
"preferred_language",
|
||||
"theme",
|
||||
]
|
||||
help_texts = {f: None for f in fields}
|
||||
widgets = {
|
||||
|
@ -454,6 +456,22 @@ class SiteForm(CustomForm):
|
|||
}
|
||||
|
||||
|
||||
class SiteThemeForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.SiteSettings
|
||||
fields = ["default_theme"]
|
||||
|
||||
|
||||
class ThemeForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Theme
|
||||
fields = ["name", "path"]
|
||||
widgets = {
|
||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
||||
"path": forms.Select(attrs={"aria-describedby": "desc_path"}),
|
||||
}
|
||||
|
||||
|
||||
class AnnouncementForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Announcement
|
||||
|
|
68
bookwyrm/migrations/0142_auto_20220227_1752.py
Normal file
68
bookwyrm/migrations/0142_auto_20220227_1752.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
# Generated by Django 3.2.12 on 2022-02-27 17:52
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def add_default_themes(apps, schema_editor):
|
||||
"""add light and dark themes"""
|
||||
db_alias = schema_editor.connection.alias
|
||||
theme_model = apps.get_model("bookwyrm", "Theme")
|
||||
theme_model.objects.using(db_alias).create(
|
||||
name="BookWyrm Light",
|
||||
path="css/themes/bookwyrm-light.scss",
|
||||
)
|
||||
theme_model.objects.using(db_alias).create(
|
||||
name="BookWyrm Dark",
|
||||
path="css/themes/bookwyrm-dark.scss",
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0141_alter_report_status"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Theme",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_date", models.DateTimeField(auto_now_add=True)),
|
||||
("name", models.CharField(max_length=50, unique=True)),
|
||||
("path", models.CharField(max_length=50, unique=True)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="sitesettings",
|
||||
name="default_theme",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="bookwyrm.theme",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="theme",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="bookwyrm.theme",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
add_default_themes, reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
19
bookwyrm/migrations/0142_user_hide_follows.py
Normal file
19
bookwyrm/migrations/0142_user_hide_follows.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 3.2.12 on 2022-02-28 19:44
|
||||
|
||||
import bookwyrm.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0141_alter_report_status"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="hide_follows",
|
||||
field=bookwyrm.models.fields.BooleanField(default=False),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,13 @@
|
|||
# Generated by Django 3.2.12 on 2022-02-28 21:28
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0142_auto_20220227_1752"),
|
||||
("bookwyrm", "0142_user_hide_follows"),
|
||||
]
|
||||
|
||||
operations = []
|
39
bookwyrm/migrations/0144_alter_announcement_display_type.py
Normal file
39
bookwyrm/migrations/0144_alter_announcement_display_type.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
# Generated by Django 3.2.12 on 2022-03-01 18:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def remove_white(apps, schema_editor):
|
||||
"""don't hardcode white announcements"""
|
||||
db_alias = schema_editor.connection.alias
|
||||
announcement_model = apps.get_model("bookwyrm", "Announcement")
|
||||
announcement_model.objects.using(db_alias).filter(display_type="white-ter").update(
|
||||
display_type=None
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0143_merge_0142_auto_20220227_1752_0142_user_hide_follows"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="announcement",
|
||||
name="display_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("primary-light", "Primary"),
|
||||
("success-light", "Success"),
|
||||
("link-light", "Link"),
|
||||
("warning-light", "Warning"),
|
||||
("danger-light", "Danger"),
|
||||
],
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(remove_white, reverse_code=migrations.RunPython.noop),
|
||||
]
|
|
@ -26,7 +26,7 @@ from .group import Group, GroupMember, GroupMemberInvitation
|
|||
|
||||
from .import_job import ImportJob, ImportItem
|
||||
|
||||
from .site import SiteSettings, SiteInvite
|
||||
from .site import SiteSettings, Theme, SiteInvite
|
||||
from .site import PasswordReset, InviteRequest
|
||||
from .announcement import Announcement
|
||||
from .antispam import EmailBlocklist, IPBlocklist, AutoMod, automod_task
|
||||
|
|
|
@ -8,7 +8,6 @@ from .base_model import BookWyrmModel
|
|||
|
||||
|
||||
DisplayTypes = [
|
||||
("white-ter", _("None")),
|
||||
("primary-light", _("Primary")),
|
||||
("success-light", _("Success")),
|
||||
("link-light", _("Link")),
|
||||
|
@ -28,11 +27,7 @@ class Announcement(BookWyrmModel):
|
|||
end_date = models.DateTimeField(blank=True, null=True)
|
||||
active = models.BooleanField(default=True)
|
||||
display_type = models.CharField(
|
||||
max_length=20,
|
||||
blank=False,
|
||||
null=False,
|
||||
choices=DisplayTypes,
|
||||
default="white-ter",
|
||||
max_length=20, choices=DisplayTypes, null=True, blank=True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -24,6 +24,9 @@ class SiteSettings(models.Model):
|
|||
)
|
||||
instance_description = models.TextField(default="This instance has no description.")
|
||||
instance_short_description = models.CharField(max_length=255, blank=True, null=True)
|
||||
default_theme = models.ForeignKey(
|
||||
"Theme", null=True, blank=True, on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
# admin setup options
|
||||
install_mode = models.BooleanField(default=False)
|
||||
|
@ -104,6 +107,18 @@ class SiteSettings(models.Model):
|
|||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Theme(models.Model):
|
||||
"""Theme files"""
|
||||
|
||||
created_date = models.DateTimeField(auto_now_add=True)
|
||||
name = models.CharField(max_length=50, unique=True)
|
||||
path = models.CharField(max_length=50, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
# pylint: disable=invalid-str-returned
|
||||
return self.name
|
||||
|
||||
|
||||
class SiteInvite(models.Model):
|
||||
"""gives someone access to create an account on the instance"""
|
||||
|
||||
|
|
|
@ -227,7 +227,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
|||
@classmethod
|
||||
def privacy_filter(cls, viewer, privacy_levels=None):
|
||||
queryset = super().privacy_filter(viewer, privacy_levels=privacy_levels)
|
||||
return queryset.filter(deleted=False)
|
||||
return queryset.filter(deleted=False, user__is_active=True)
|
||||
|
||||
@classmethod
|
||||
def direct_filter(cls, queryset, viewer):
|
||||
|
|
|
@ -136,6 +136,8 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
updated_date = models.DateTimeField(auto_now=True)
|
||||
last_active_date = models.DateTimeField(default=timezone.now)
|
||||
manually_approves_followers = fields.BooleanField(default=False)
|
||||
theme = models.ForeignKey("Theme", null=True, blank=True, on_delete=models.SET_NULL)
|
||||
hide_follows = fields.BooleanField(default=False)
|
||||
|
||||
# options to turn features on and off
|
||||
show_goal = models.BooleanField(default=True)
|
||||
|
@ -478,10 +480,13 @@ def set_remote_server(user_id):
|
|||
get_remote_reviews.delay(user.outbox)
|
||||
|
||||
|
||||
def get_or_create_remote_server(domain):
|
||||
def get_or_create_remote_server(domain, refresh=False):
|
||||
"""get info on a remote server"""
|
||||
server = FederatedServer()
|
||||
try:
|
||||
return FederatedServer.objects.get(server_name=domain)
|
||||
server = FederatedServer.objects.get(server_name=domain)
|
||||
if not refresh:
|
||||
return server
|
||||
except FederatedServer.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
@ -496,13 +501,15 @@ def get_or_create_remote_server(domain):
|
|||
application_type = data.get("software", {}).get("name")
|
||||
application_version = data.get("software", {}).get("version")
|
||||
except ConnectorException:
|
||||
if server.id:
|
||||
return server
|
||||
application_type = application_version = None
|
||||
|
||||
server = FederatedServer.objects.create(
|
||||
server_name=domain,
|
||||
application_type=application_type,
|
||||
application_version=application_version,
|
||||
)
|
||||
server.server_name = domain
|
||||
server.application_type = application_type
|
||||
server.application_version = application_version
|
||||
|
||||
server.save()
|
||||
return server
|
||||
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
env = Env()
|
||||
env.read_env()
|
||||
DOMAIN = env("DOMAIN")
|
||||
VERSION = "0.3.1"
|
||||
VERSION = "0.3.2"
|
||||
|
||||
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 = "a60e5a55"
|
||||
JS_CACHE = "c7154efb"
|
||||
|
||||
# email
|
||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
@charset "utf-8";
|
||||
|
||||
@import "instance-settings";
|
||||
@import "themes/light.scss";
|
||||
@import "vendor/bulma/bulma.sass";
|
||||
@import "vendor/icons.css";
|
||||
@import "bookwyrm/all.scss";
|
||||
|
|
|
@ -115,7 +115,7 @@ button .button-invisible-overlay {
|
|||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background: rgba($scheme-invert, 0.66);
|
||||
background: $invisible-overlay-background-color;
|
||||
color: white;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
|
|
|
@ -53,7 +53,7 @@ details.dropdown .dropdown-menu a:focus-visible {
|
|||
|
||||
@media only screen and (max-width: 768px) {
|
||||
details.dropdown[open] summary.dropdown-trigger::before {
|
||||
background-color: rgba($scheme-invert, 0.5);
|
||||
background-color: $modal-background-background-color;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
.toggle-button[aria-pressed="true"],
|
||||
.toggle-button[aria-pressed="true"]:hover {
|
||||
background-color: hsl(171deg, 100%, 41%);
|
||||
background-color: $primary;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
|
84
bookwyrm/static/css/themes/bookwyrm-dark.scss
Normal file
84
bookwyrm/static/css/themes/bookwyrm-dark.scss
Normal file
|
@ -0,0 +1,84 @@
|
|||
@import "../vendor/bulma/sass/utilities/initial-variables.sass";
|
||||
|
||||
/* Colors
|
||||
******************************************************************************/
|
||||
|
||||
/* states */
|
||||
$primary: #005e50;
|
||||
$primary-light: #1d2b28;
|
||||
$info: #1f4666;
|
||||
$success: #246447;
|
||||
$warning: #8b6c15;
|
||||
$danger: #872538;
|
||||
$danger-light: #481922;
|
||||
$light: #393939;
|
||||
$red: #ffa1b4;
|
||||
|
||||
/* book cover standins */
|
||||
$no-cover-color: #002549;
|
||||
|
||||
/* background colors */
|
||||
$scheme-main: rgb(24, 27, 28);
|
||||
$scheme-invert: #fff;
|
||||
$scheme-main-bis: rgb(28, 30, 32);
|
||||
$scheme-main-ter: rgb(32, 34, 36);
|
||||
$background-body: rgb(24, 27, 28);
|
||||
$background-secondary: rgb(28, 30, 32);
|
||||
$background-tertiary: rgb(32, 34, 36);
|
||||
$modal-background-background-color: rgba($black, 0.8);
|
||||
|
||||
/* highlight colors */
|
||||
$primary-highlight: $primary;
|
||||
$info-highlight: $info;
|
||||
$success-highlight: $success;
|
||||
|
||||
/* borders */
|
||||
$border: #2b3031;
|
||||
$border-light: #444;
|
||||
$border-hover: #51595d;
|
||||
|
||||
/* text */
|
||||
$text: $grey-lightest;
|
||||
$text-light: $grey-lighter;
|
||||
$text-strong: $white-ter;
|
||||
|
||||
/* links */
|
||||
$link: #2e7eb9;
|
||||
$link-background: $background-tertiary;
|
||||
$link-hover: $white-bis;
|
||||
$link-hover-border: #51595d;
|
||||
$link-focus: $white-bis;
|
||||
$link-active: $white-bis;
|
||||
|
||||
/* bulma overrides */
|
||||
$background: $background-secondary;
|
||||
$menu-item-active-background-color: $link-background;
|
||||
$navbar-dropdown-item-hover-color: $white;
|
||||
|
||||
/* These element's colors are hardcoded, probably a bug in bulma? */
|
||||
@media screen and (min-width: 769px) {
|
||||
.navbar-dropdown {
|
||||
box-shadow: 0 8px 8px rgba($black, 0.2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.navbar-menu {
|
||||
box-shadow: 0 8px 8px rgba($black, 0.2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* misc */
|
||||
$shadow: 0 0.5em 1em -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;
|
||||
|
||||
/* Fonts
|
||||
******************************************************************************/
|
||||
$family-primary: $family-sans-serif;
|
||||
$family-secondary: $family-sans-serif;
|
||||
|
||||
|
||||
@import "../bookwyrm.scss";
|
||||
@import "../vendor/icons.css";
|
|
@ -47,7 +47,13 @@ $link-active: $grey-darker;
|
|||
$background: $background-secondary;
|
||||
$menu-item-active-background-color: $link-background;
|
||||
|
||||
/* misc */
|
||||
$invisible-overlay-background-color: rgba($scheme-invert, 0.66);
|
||||
|
||||
/* Fonts
|
||||
******************************************************************************/
|
||||
$family-primary: $family-sans-serif;
|
||||
$family-secondary: $family-sans-serif;
|
||||
|
||||
@import "../bookwyrm.scss";
|
||||
@import "../vendor/icons.css";
|
|
@ -1,55 +0,0 @@
|
|||
@import "../vendor/bulma/sass/utilities/derived-variables.sass";
|
||||
|
||||
/* Colors
|
||||
******************************************************************************/
|
||||
|
||||
/* states */
|
||||
$primary: #016a5b;
|
||||
$info: #1f4666;
|
||||
$success: #246447;
|
||||
$warning: #8b6c15;
|
||||
$danger: #872538;
|
||||
|
||||
/* book cover standins */
|
||||
$no-cover-color: #002549;
|
||||
|
||||
/* background colors */
|
||||
$scheme-main: $grey-darker;
|
||||
$scheme-main-bis: $black-ter;
|
||||
$background-body: $grey-darker;
|
||||
$background-secondary: $grey-dark;
|
||||
$background-tertiary: #555;
|
||||
|
||||
/* highlight colors */
|
||||
$primary-highlight: $primary;
|
||||
$info-highlight: $info;
|
||||
$success-highlight: $success;
|
||||
|
||||
/* borders */
|
||||
$border: $grey;
|
||||
$border-hover: $grey-light;
|
||||
$border-light: $grey;
|
||||
$border-light-hover: $grey-light;
|
||||
|
||||
/* text */
|
||||
$text: $grey-lightest;
|
||||
$text-light: $grey-lighter;
|
||||
$text-strong: $white-ter;
|
||||
|
||||
/* links */
|
||||
$link: $white;
|
||||
$link-background: $background-tertiary;
|
||||
$link-hover: $white-bis;
|
||||
$link-focus: $white-bis;
|
||||
$link-active: $white-bis;
|
||||
|
||||
/* misc */
|
||||
|
||||
/* bulma overrides */
|
||||
$background: $background-secondary;
|
||||
$menu-item-active-background-color: $link-background;
|
||||
|
||||
/* Fonts
|
||||
******************************************************************************/
|
||||
$family-primary: $family-sans-serif;
|
||||
$family-secondary: $family-sans-serif;
|
|
@ -1,11 +1,11 @@
|
|||
/* exported TabGroup */
|
||||
|
||||
/*
|
||||
* The content below is licensed according to the W3C Software License at
|
||||
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
|
||||
* Heavily modified to web component by Zach Leatherman
|
||||
* Modified back to vanilla JavaScript with support for Bulma markup and nested tabs by Ned Zimmerman
|
||||
*/
|
||||
* The content below is licensed according to the W3C Software License at
|
||||
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
|
||||
* Heavily modified to web component by Zach Leatherman
|
||||
* Modified back to vanilla JavaScript with support for Bulma markup and nested tabs by Ned Zimmerman
|
||||
*/
|
||||
class TabGroup {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
|
@ -15,7 +15,7 @@ class TabGroup {
|
|||
this.panels = this.container.querySelectorAll(':scope > [role="tabpanel"]');
|
||||
this.delay = this.determineDelay();
|
||||
|
||||
if(!this.tablist || !this.tabs.length || !this.panels.length) {
|
||||
if (!this.tablist || !this.tabs.length || !this.panels.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ class TabGroup {
|
|||
left: 37,
|
||||
up: 38,
|
||||
right: 39,
|
||||
down: 40
|
||||
down: 40,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -42,23 +42,21 @@ class TabGroup {
|
|||
37: -1,
|
||||
38: -1,
|
||||
39: 1,
|
||||
40: 1
|
||||
40: 1,
|
||||
};
|
||||
}
|
||||
|
||||
initTabs() {
|
||||
let count = 0;
|
||||
for(let tab of this.tabs) {
|
||||
|
||||
for (let tab of this.tabs) {
|
||||
let isSelected = tab.getAttribute("aria-selected") === "true";
|
||||
|
||||
tab.setAttribute("tabindex", isSelected ? "0" : "-1");
|
||||
|
||||
tab.addEventListener('click', this.clickEventListener.bind(this));
|
||||
tab.addEventListener('keydown', this.keydownEventListener.bind(this));
|
||||
tab.addEventListener('keyup', this.keyupEventListener.bind(this));
|
||||
|
||||
if (isSelected) {
|
||||
tab.scrollIntoView();
|
||||
}
|
||||
tab.addEventListener("click", this.clickEventListener.bind(this));
|
||||
tab.addEventListener("keydown", this.keydownEventListener.bind(this));
|
||||
tab.addEventListener("keyup", this.keyupEventListener.bind(this));
|
||||
|
||||
tab.index = count++;
|
||||
}
|
||||
|
@ -68,8 +66,9 @@ class TabGroup {
|
|||
let selectedPanelId = this.tablist
|
||||
.querySelector('[role="tab"][aria-selected="true"]')
|
||||
.getAttribute("aria-controls");
|
||||
for(let panel of this.panels) {
|
||||
if(panel.getAttribute("id") !== selectedPanelId) {
|
||||
|
||||
for (let panel of this.panels) {
|
||||
if (panel.getAttribute("id") !== selectedPanelId) {
|
||||
panel.setAttribute("hidden", "");
|
||||
}
|
||||
panel.setAttribute("tabindex", "0");
|
||||
|
@ -86,16 +85,18 @@ class TabGroup {
|
|||
|
||||
// Handle keydown on tabs
|
||||
keydownEventListener(event) {
|
||||
var key = event.keyCode;
|
||||
const key = event.keyCode;
|
||||
|
||||
switch (key) {
|
||||
case this.keys.end:
|
||||
event.preventDefault();
|
||||
|
||||
// Activate last tab
|
||||
this.activateTab(this.tabs[this.tabs.length - 1]);
|
||||
break;
|
||||
case this.keys.home:
|
||||
event.preventDefault();
|
||||
|
||||
// Activate first tab
|
||||
this.activateTab(this.tabs[0]);
|
||||
break;
|
||||
|
@ -111,7 +112,7 @@ class TabGroup {
|
|||
|
||||
// Handle keyup on tabs
|
||||
keyupEventListener(event) {
|
||||
var key = event.keyCode;
|
||||
const key = event.keyCode;
|
||||
|
||||
switch (key) {
|
||||
case this.keys.left:
|
||||
|
@ -125,17 +126,16 @@ class TabGroup {
|
|||
// only up and down arrow should function.
|
||||
// In all other cases only left and right arrow function.
|
||||
determineOrientation(event) {
|
||||
var key = event.keyCode;
|
||||
var vertical = this.tablist.getAttribute('aria-orientation') == 'vertical';
|
||||
var proceed = false;
|
||||
const key = event.keyCode;
|
||||
const vertical = this.tablist.getAttribute("aria-orientation") == "vertical";
|
||||
let proceed = false;
|
||||
|
||||
if (vertical) {
|
||||
if (key === this.keys.up || key === this.keys.down) {
|
||||
event.preventDefault();
|
||||
proceed = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (key === this.keys.left || key === this.keys.right) {
|
||||
proceed = true;
|
||||
}
|
||||
|
@ -149,22 +149,21 @@ class TabGroup {
|
|||
// Either focus the next, previous, first, or last tab
|
||||
// depending on key pressed
|
||||
switchTabOnArrowPress(event) {
|
||||
var pressed = event.keyCode;
|
||||
const pressed = event.keyCode;
|
||||
|
||||
for (let tab of this.tabs) {
|
||||
tab.addEventListener('focus', this.focusEventHandler.bind(this));
|
||||
tab.addEventListener("focus", this.focusEventHandler.bind(this));
|
||||
}
|
||||
|
||||
if (this.direction[pressed]) {
|
||||
var target = event.target;
|
||||
const target = event.target;
|
||||
|
||||
if (target.index !== undefined) {
|
||||
if (this.tabs[target.index + this.direction[pressed]]) {
|
||||
this.tabs[target.index + this.direction[pressed]].focus();
|
||||
}
|
||||
else if (pressed === this.keys.left || pressed === this.keys.up) {
|
||||
} else if (pressed === this.keys.left || pressed === this.keys.up) {
|
||||
this.focusLastTab();
|
||||
}
|
||||
else if (pressed === this.keys.right || pressed == this.keys.down) {
|
||||
} else if (pressed === this.keys.right || pressed == this.keys.down) {
|
||||
this.focusFirstTab();
|
||||
}
|
||||
}
|
||||
|
@ -172,8 +171,8 @@ class TabGroup {
|
|||
}
|
||||
|
||||
// Activates any given tab panel
|
||||
activateTab (tab, setFocus) {
|
||||
if(tab.getAttribute("role") !== "tab") {
|
||||
activateTab(tab, setFocus) {
|
||||
if (tab.getAttribute("role") !== "tab") {
|
||||
tab = tab.closest('[role="tab"]');
|
||||
}
|
||||
|
||||
|
@ -183,19 +182,19 @@ class TabGroup {
|
|||
this.deactivateTabs();
|
||||
|
||||
// Remove tabindex attribute
|
||||
tab.removeAttribute('tabindex');
|
||||
tab.removeAttribute("tabindex");
|
||||
|
||||
// Set the tab as selected
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
tab.setAttribute("aria-selected", "true");
|
||||
|
||||
// Give the tab is-active class
|
||||
tab.classList.add('is-active');
|
||||
tab.classList.add("is-active");
|
||||
|
||||
// Get the value of aria-controls (which is an ID)
|
||||
var controls = tab.getAttribute('aria-controls');
|
||||
const controls = tab.getAttribute("aria-controls");
|
||||
|
||||
// Remove hidden attribute from tab panel to make it visible
|
||||
document.getElementById(controls).removeAttribute('hidden');
|
||||
document.getElementById(controls).removeAttribute("hidden");
|
||||
|
||||
// Set focus when required
|
||||
if (setFocus) {
|
||||
|
@ -206,14 +205,14 @@ class TabGroup {
|
|||
// Deactivate all tabs and tab panels
|
||||
deactivateTabs() {
|
||||
for (let tab of this.tabs) {
|
||||
tab.classList.remove('is-active');
|
||||
tab.setAttribute('tabindex', '-1');
|
||||
tab.setAttribute('aria-selected', 'false');
|
||||
tab.removeEventListener('focus', this.focusEventHandler.bind(this));
|
||||
tab.classList.remove("is-active");
|
||||
tab.setAttribute("tabindex", "-1");
|
||||
tab.setAttribute("aria-selected", "false");
|
||||
tab.removeEventListener("focus", this.focusEventHandler.bind(this));
|
||||
}
|
||||
|
||||
for (let panel of this.panels) {
|
||||
panel.setAttribute('hidden', 'hidden');
|
||||
panel.setAttribute("hidden", "hidden");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,15 +227,15 @@ class TabGroup {
|
|||
// Determine whether there should be a delay
|
||||
// when user navigates with the arrow keys
|
||||
determineDelay() {
|
||||
var hasDelay = this.tablist.hasAttribute('data-delay');
|
||||
var delay = 0;
|
||||
const hasDelay = this.tablist.hasAttribute("data-delay");
|
||||
let delay = 0;
|
||||
|
||||
if (hasDelay) {
|
||||
var delayValue = this.tablist.getAttribute('data-delay');
|
||||
const delayValue = this.tablist.getAttribute("data-delay");
|
||||
|
||||
if (delayValue) {
|
||||
delay = delayValue;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// If no value is specified, default to 300ms
|
||||
delay = 300;
|
||||
}
|
||||
|
@ -246,7 +245,7 @@ class TabGroup {
|
|||
}
|
||||
|
||||
focusEventHandler(event) {
|
||||
var target = event.target;
|
||||
const target = event.target;
|
||||
|
||||
setTimeout(this.checkTabFocus.bind(this), this.delay, target);
|
||||
}
|
|
@ -14,23 +14,25 @@
|
|||
{% cache 604800 about_page %}
|
||||
|
||||
{% get_book_superlatives as superlatives %}
|
||||
<section class="content pb-4">
|
||||
<h2>
|
||||
{% blocktrans with site_name=site.name %}Welcome to {{ site_name }}!{% endblocktrans %}
|
||||
</h2>
|
||||
<section class=" pb-4">
|
||||
<div class="content">
|
||||
<h2>
|
||||
{% blocktrans with site_name=site.name %}Welcome to {{ site_name }}!{% endblocktrans %}
|
||||
</h2>
|
||||
|
||||
<p class="subtitle notification has-background-primary-highlight">
|
||||
{% blocktrans trimmed with site_name=site.name %}
|
||||
{{ site_name }} is part of <em>BookWyrm</em>, a network of independent, self-directed communities for readers.
|
||||
While you can interact seamlessly with users anywhere in the <a href="https://joinbookwyrm.com/instances/" target="_blank">BookWyrm network</a>, this community is unique.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p class="subtitle notification has-background-primary-highlight">
|
||||
{% blocktrans trimmed with site_name=site.name %}
|
||||
{{ site_name }} is part of <em>BookWyrm</em>, a network of independent, self-directed communities for readers.
|
||||
While you can interact seamlessly with users anywhere in the <a href="https://joinbookwyrm.com/instances/" target="_blank">BookWyrm network</a>, this community is unique.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
{% if superlatives.top_rated %}
|
||||
{% with book=superlatives.top_rated.default_edition rating=superlatives.top_rated.rating %}
|
||||
<div class="column is-one-third is-flex">
|
||||
<div class="media notification">
|
||||
<div class="media notification is-clipped">
|
||||
<div class="media-left">
|
||||
<a href="{{ book.local_path }}">
|
||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %}
|
||||
|
@ -49,7 +51,7 @@
|
|||
{% if superlatives.wanted %}
|
||||
{% with book=superlatives.wanted.default_edition %}
|
||||
<div class="column is-one-third is-flex">
|
||||
<div class="media notification">
|
||||
<div class="media notification is-clipped">
|
||||
<div class="media-left">
|
||||
<a href="{{ book.local_path }}">
|
||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %}
|
||||
|
@ -68,7 +70,7 @@
|
|||
{% if superlatives.controversial %}
|
||||
{% with book=superlatives.controversial.default_edition %}
|
||||
<div class="column is-one-third is-flex">
|
||||
<div class="media notification">
|
||||
<div class="media notification is-clipped">
|
||||
<div class="media-left">
|
||||
<a href="{{ book.local_path }}">
|
||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %}
|
||||
|
|
|
@ -352,7 +352,7 @@
|
|||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if request.user.list_set.exists %}
|
||||
{% if list_options.exists %}
|
||||
<form name="list-add" method="post" action="{% url 'list-add-book' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
|
@ -361,7 +361,7 @@
|
|||
<div class="field has-addons">
|
||||
<div class="select control is-clipped">
|
||||
<select name="book_list" id="id_list">
|
||||
{% for list in user.list_set.all %}
|
||||
{% for list in list_options %}
|
||||
<option value="{{ list.id }}">{{ list.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
@ -386,6 +386,6 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{% static "js/vendor/tabs.js" %}?v={{ js_cache }}"></script>
|
||||
<script src="{% static "js/tabs.js" %}?v={{ js_cache }}"></script>
|
||||
<script src="{% static "js/autocomplete.js" %}?v={{ js_cache }}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
<footer class="card-footer">
|
||||
{% if user != request.user %}
|
||||
{% if user.mutuals %}
|
||||
{% if user.mutuals and not user.hide_follows %}
|
||||
<div class="card-footer-item">
|
||||
<div class="has-text-centered">
|
||||
<p class="title is-6 mb-0">{{ user.mutuals }}</p>
|
||||
|
|
|
@ -31,5 +31,5 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{% static "js/vendor/tabs.js" %}?v={{ js_cache }}"></script>
|
||||
<script src="{% static "js/tabs.js" %}?v={{ js_cache }}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
|
||||
</a>
|
||||
{% include 'snippets/add_to_group_button.html' with user=user group=group %}
|
||||
{% if user.mutuals %}
|
||||
{% if user.mutuals and not user.hide_follows %}
|
||||
<p class="help">
|
||||
{% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %}
|
||||
{{ mutuals }} follower you follow
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<head>
|
||||
<title>{% block title %}BookWyrm{% endblock %} - {{ site.name }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="{% sass_src 'css/bookwyrm.scss' %}" rel="stylesheet" type="text/css" />
|
||||
<link href="{% sass_src site_theme %}" rel="stylesheet" type="text/css" />
|
||||
|
||||
<link rel="search" type="application/opensearchdescription+xml" href="{% url 'opensearch' %}" title="{% blocktrans with site_name=site.name %}{{ site_name }} search{% endblocktrans %}" />
|
||||
|
||||
|
|
|
@ -30,13 +30,23 @@
|
|||
|
||||
<div class="columns mt-3">
|
||||
<section class="column is-three-quarters">
|
||||
{% if request.GET.updated %}
|
||||
<div class="notification is-primary">
|
||||
{% if list.curation != "open" and request.user != list.user and not list.group|is_member:request.user %}
|
||||
{% trans "You successfully suggested a book for this list!" %}
|
||||
{% else %}
|
||||
{% trans "You successfully added a book to this list!" %}
|
||||
{% endif %}
|
||||
{% if add_failed %}
|
||||
<div class="notification is-danger is-light">
|
||||
<span class="icon icon-x" aria-hidden="true"></span>
|
||||
<span>
|
||||
{% trans "That book is already on this list." %}
|
||||
</span>
|
||||
</div>
|
||||
{% elif add_succeeded %}
|
||||
<div class="notification is-success is-light">
|
||||
<span class="icon icon-check" aria-hidden="true"></span>
|
||||
<span>
|
||||
{% if list.curation != "open" and request.user != list.user and not list.group|is_member:request.user %}
|
||||
{% trans "You successfully suggested a book for this list!" %}
|
||||
{% else %}
|
||||
{% trans "You successfully added a book to this list!" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
{% block profile-tabs %}
|
||||
<ul class="menu-list">
|
||||
<li><a href="#profile">{% trans "Profile" %}</a></li>
|
||||
<li><a href="#display-preferences">{% trans "Display preferences" %}</a></li>
|
||||
<li><a href="#display-preferences">{% trans "Display" %}</a></li>
|
||||
<li><a href="#privacy">{% trans "Privacy" %}</a></li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
@ -61,7 +61,7 @@
|
|||
<hr aria-hidden="true">
|
||||
|
||||
<section class="block" id="display-preferences">
|
||||
<h2 class="title is-4">{% trans "Display preferences" %}</h2>
|
||||
<h2 class="title is-4">{% trans "Display" %}</h2>
|
||||
<div class="box">
|
||||
<div class="field">
|
||||
<label class="checkbox label" for="id_show_goal">
|
||||
|
@ -97,6 +97,12 @@
|
|||
{{ form.preferred_language }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_them">{% trans "Theme:" %}</label>
|
||||
<div class="select">
|
||||
{{ form.theme }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
@ -111,6 +117,12 @@
|
|||
{% trans "Manually approve followers" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox label" for="id_hide_follows">
|
||||
{{ form.hide_follows }}
|
||||
{% trans "Hide followers and following on profile" %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_default_post_privacy">
|
||||
{% trans "Default post privacy:" %}
|
||||
|
|
|
@ -4,7 +4,19 @@
|
|||
|
||||
{% block header %}
|
||||
{% trans "Add instance" %}
|
||||
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to instance list" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{% url 'settings-federation' %}">{% trans "Federated Instances" %}</a></li>
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">
|
||||
{% trans "Add instance" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
|
@ -73,9 +85,13 @@
|
|||
<label class="label" for="id_notes">
|
||||
{% trans "Notes:" %}
|
||||
</label>
|
||||
<textarea name="notes" cols="40" rows="5" class="textarea" id="id_notes">
|
||||
{{ form.notes.value|default:'' }}
|
||||
</textarea>
|
||||
<textarea
|
||||
name="notes"
|
||||
cols="40"
|
||||
rows="5"
|
||||
class="textarea"
|
||||
id="id_notes"
|
||||
>{{ form.notes.value|default:'' }}</textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button is-primary">
|
||||
|
|
|
@ -9,8 +9,26 @@
|
|||
|
||||
{% if server.status == "blocked" %}<span class="icon icon-x has-text-danger is-size-5" title="{% trans 'Blocked' %}"><span class="is-sr-only">{% trans "Blocked" %}</span></span>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to list" %}</a>
|
||||
{% block edit-button %}
|
||||
<form name="reload" method="POST" action="{% url 'settings-federated-server-refresh' server.id %}">
|
||||
{% csrf_token %}
|
||||
<button class="button" type="submit">{% trans "Refresh data" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{% url 'settings-federation' %}">{% trans "Federated Instances" %}</a></li>
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">
|
||||
{{ server.server_name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
|
|
|
@ -4,7 +4,19 @@
|
|||
|
||||
{% block header %}
|
||||
{% trans "Import Blocklist" %}
|
||||
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to instance list" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<nav class="breadcrumb subtitle" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="{% url 'settings-federation' %}">{% trans "Federated Instances" %}</a></li>
|
||||
<li class="is-active">
|
||||
<a href="#" aria-current="page">
|
||||
{% trans "Import Blocklist" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
|
|
|
@ -16,11 +16,11 @@
|
|||
<ul>
|
||||
{% url 'settings-federation' status='federated' as url %}
|
||||
<li {% if request.path in url %}class="is-active" aria-current="page"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Federated" %}</a>
|
||||
<a href="{{ url }}">{% trans "Federated" %} ({{ federated_count }})</a>
|
||||
</li>
|
||||
{% url 'settings-federation' status='blocked' as url %}
|
||||
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
|
||||
<a href="{{ url }}">{% trans "Blocked" %}</a>
|
||||
<a href="{{ url }}">{% trans "Blocked" %} ({{ blocked_count }})</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -86,6 +86,10 @@
|
|||
<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>
|
||||
{% url 'settings-themes' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Themes" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
{% block site-subtabs %}
|
||||
<ul class="menu-list">
|
||||
<li><a href="#instance-info">{% trans "Instance Info" %}</a></li>
|
||||
<li><a href="#images">{% trans "Images" %}</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>
|
||||
|
@ -33,7 +33,12 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="{% url 'settings-site' %}" method="POST" class="content" enctype="multipart/form-data">
|
||||
<form
|
||||
action="{% url 'settings-site' %}"
|
||||
method="POST"
|
||||
class="content"
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<section class="block" id="instance_info">
|
||||
<h2 class="title is-4">{% trans "Instance Info" %}</h2>
|
||||
|
@ -68,20 +73,33 @@
|
|||
|
||||
<hr aria-hidden="true">
|
||||
|
||||
<section class="block" id="images">
|
||||
<h2 class="title is-4">{% trans "Images" %}</h2>
|
||||
<div class="box is-flex">
|
||||
<div>
|
||||
<label class="label" for="id_logo">{% trans "Logo:" %}</label>
|
||||
{{ site_form.logo }}
|
||||
<section class="block" id="display">
|
||||
<h2 class="title is-4">{% trans "Display" %}</h2>
|
||||
<div class="box">
|
||||
<h3 class="title is-5">{% trans "Images" %}</h3>
|
||||
<div class="block is-flex">
|
||||
<div>
|
||||
<label class="label" for="id_logo">{% trans "Logo:" %}</label>
|
||||
{{ site_form.logo }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="id_logo_small">{% trans "Logo small:" %}</label>
|
||||
{{ site_form.logo_small }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="id_favicon">{% trans "Favicon:" %}</label>
|
||||
{{ site_form.favicon }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="id_logo_small">{% trans "Logo small:" %}</label>
|
||||
{{ site_form.logo_small }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="id_favicon">{% trans "Favicon:" %}</label>
|
||||
{{ site_form.favicon }}
|
||||
|
||||
<h3 class="title is-5">{% trans "Themes" %}</h3>
|
||||
<div class="block">
|
||||
<label class="label" for="id_default_theme">
|
||||
{% trans "Default theme:" %}
|
||||
</label>
|
||||
<div class="select">
|
||||
{{ site_form.default_theme }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
140
bookwyrm/templates/settings/themes.html
Normal file
140
bookwyrm/templates/settings/themes.html
Normal file
|
@ -0,0 +1,140 @@
|
|||
{% extends 'settings/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Themes" %}{% endblock %}
|
||||
|
||||
{% block header %}{% trans "Themes" %}{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<a class="subtitle help is-link" href="{% url 'settings-site' %}/#display">
|
||||
{% trans "Set instance default theme" %}
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
{% if success %}
|
||||
<div class="notification is-success is-light">
|
||||
<span class="icon icon-check" aria-hidden="true"></span>
|
||||
<span>
|
||||
{% trans "Successfully added theme" %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<section class="block">
|
||||
<div class="notification content">
|
||||
<h2 class="title is-5">{% trans "How to add a theme" %}</h2>
|
||||
<ol>
|
||||
<li>
|
||||
{% trans "Copy the theme file into the <code>bookwyrm/static/css/themes</code> directory on your server from the command line." %}
|
||||
</li>
|
||||
<li>
|
||||
{% trans "Run <code>./bw-dev compilescss</code>." %}
|
||||
</li>
|
||||
<li>
|
||||
{% trans "Add the file name using the form below to make it available in the application interface." %}
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block content">
|
||||
<h2 class="title is-4">{% trans "Add theme" %}</h2>
|
||||
|
||||
{% if theme_form.errors %}
|
||||
<div class="notification is-danger is-light">
|
||||
<span class="icon icon-x" aria-hidden="true"></span>
|
||||
<span>
|
||||
{% trans "Unable to save theme" %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="{% url 'settings-themes' %}"
|
||||
class="box"
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
{% if not choices %}
|
||||
<div class="notification is-warning">
|
||||
{% trans "No available theme files detected" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<fieldset {% if not choices %}disabled{% endif %}>
|
||||
{% csrf_token %}
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<label class="label" for="id_name">
|
||||
{% trans "Theme name" %}
|
||||
</label>
|
||||
<div class="control">
|
||||
{{ theme_form.name }}
|
||||
{% include 'snippets/form_errors.html' with errors_list=theme_form.name.errors id="desc_name" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<label class="label" for="id_path">
|
||||
{% trans "Theme filename" %}
|
||||
</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select
|
||||
name="path"
|
||||
aria-describedby="desc_path"
|
||||
class=""
|
||||
id="id_path"
|
||||
>
|
||||
{% for choice in choices %}
|
||||
<option value="{{ choice }}">
|
||||
{{ choice }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% include 'snippets/form_errors.html' with errors_list=theme_form.path.errors id="desc_path" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button">{% trans "Add theme" %}</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="block content">
|
||||
<h2 class="title is-4">{% trans "Available Themes" %}</h2>
|
||||
<div class="table-container">
|
||||
<table class="table is-striped">
|
||||
<tr>
|
||||
<th>
|
||||
{% trans "Theme name" %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "File" %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Actions" %}
|
||||
</th>
|
||||
</tr>
|
||||
{% for theme in themes %}
|
||||
<tr>
|
||||
<td>{{ theme.name }}</td>
|
||||
<td><code>{{ theme.path }}</code></td>
|
||||
<td>
|
||||
<form method="POST" action="{% url 'settings-themes-delete' theme.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="button is-danger is-light is-small">
|
||||
<span class="icon icon-x" aria-hideen="true"></span>
|
||||
<span>{% trans "Remove theme" %}</span>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
|
@ -1,7 +1,7 @@
|
|||
{% load humanize %}{% load i18n %}{% load utilities %}
|
||||
{% with announcement.id|uuid as uuid %}
|
||||
<aside
|
||||
class="notification mb-1 p-3{% if not admin_mode %} is-hidden{% endif %} transition-y has-background-{{ announcement.display_type }}"
|
||||
class="notification mb-1 p-3{% if not admin_mode %} is-hidden{% endif %} transition-y {% if announcement.display_type %}has-background-{{ announcement.display_type }}{% endif %}"
|
||||
{% if not admin_mode %}data-hide="hide_announcement_{{ announcement.id }}"{% endif %}
|
||||
>
|
||||
<details>
|
||||
|
|
|
@ -71,7 +71,9 @@
|
|||
{% csrf_token %}
|
||||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
<input type="hidden" name="shelf" value="{{ user_shelf.id }}">
|
||||
<button class="button is-fullwidth is-small is-radiusless is-danger is-light" type="submit">{% trans "Remove from" %} {{ user_shelf.name }}</button>
|
||||
<button class="button is-fullwidth is-small is-radiusless is-danger is-light" type="submit">
|
||||
{% blocktrans with name=user_shelf|translate_shelf_name %}Remove from {{ name }}{% endblocktrans %}
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
|
||||
<input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}">
|
||||
<button class="button is-fullwidth is-small{% if dropdown %} is-radiusless{% endif %} is-danger is-light" type="submit">
|
||||
{% blocktrans with name=active_shelf.shelf.name %}Remove from {{ name }}{% endblocktrans %}
|
||||
{% blocktrans with name=active_shelf.shelf|translate_shelf_name %}Remove from {{ name }}{% endblocktrans %}
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
|
|
|
@ -69,7 +69,6 @@
|
|||
|
||||
{% block card-bonus %}
|
||||
{% if request.user.is_authenticated and not moderation_mode and not no_interact %}
|
||||
{% with status.id|uuid as uuid %}
|
||||
<section class="reply-panel is-hidden" id="show_comment_{{ status.id }}">
|
||||
<div class="card-footer">
|
||||
<div class="card-footer-item">
|
||||
|
@ -77,6 +76,5 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
|
||||
</a>
|
||||
{% include 'snippets/follow_button.html' with user=user minimal=True %}
|
||||
{% if user.mutuals %}
|
||||
{% if user.mutuals and not user.hide_follows %}
|
||||
<p class="help">
|
||||
{% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %}
|
||||
{{ mutuals }} follower you follow
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
{% load i18n %}
|
||||
{% if shelf.identifier == 'all' %}
|
||||
{% trans "All books" %}
|
||||
{% elif shelf.identifier == 'to-read' %}
|
||||
{% trans "To Read" %}
|
||||
{% elif shelf.identifier == 'reading' %}
|
||||
{% trans "Currently Reading" %}
|
||||
{% elif shelf.identifier == 'read' %}
|
||||
{% trans "Read" %}
|
||||
{% else %}
|
||||
{{ shelf.name }}
|
||||
{% endif %}
|
||||
{% load shelf_tags %}
|
||||
|
||||
{{ shelf|translate_shelf_name }}
|
||||
|
|
|
@ -28,16 +28,22 @@
|
|||
|
||||
{% elif request.user.is_authenticated %}
|
||||
|
||||
{% mutuals_count user as mutuals %}
|
||||
<a href="{% url 'user-followers' user|username %}">
|
||||
{% if mutuals %}
|
||||
{% blocktrans with mutuals_display=mutuals|intcomma count counter=mutuals %}{{ mutuals_display }} follower you follow{% plural %}{{ mutuals_display }} followers you follow{% endblocktrans %}
|
||||
{% elif request.user in user.following.all %}
|
||||
{% trans "Follows you" %}
|
||||
{% else %}
|
||||
{% trans "No followers you follow" %}
|
||||
{% if user.hide_follows %}
|
||||
{% if request.user in user.following.all %}
|
||||
{% trans "Follows you" %}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
{% mutuals_count user as mutuals %}
|
||||
<a href="{% url 'user-followers' user|username %}">
|
||||
{% if mutuals %}
|
||||
{% blocktrans with mutuals_display=mutuals|intcomma count counter=mutuals %}{{ mutuals_display }} follower you follow{% plural %}{{ mutuals_display }} followers you follow{% endblocktrans %}
|
||||
{% elif request.user in user.following.all %}
|
||||
{% trans "Follows you" %}
|
||||
{% else %}
|
||||
{% trans "No followers you follow" %}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
</p>
|
||||
|
|
|
@ -16,11 +16,15 @@ def get_book_superlatives():
|
|||
models.Work.objects.annotate(
|
||||
rating=Avg(
|
||||
"editions__review__rating",
|
||||
filter=Q(editions__review__local=True, editions__review__deleted=False),
|
||||
filter=Q(
|
||||
editions__review__user__local=True, editions__review__deleted=False
|
||||
),
|
||||
),
|
||||
rating_count=Count(
|
||||
"editions__review",
|
||||
filter=Q(editions__review__local=True, editions__review__deleted=False),
|
||||
filter=Q(
|
||||
editions__review__user__local=True, editions__review__deleted=False
|
||||
),
|
||||
),
|
||||
)
|
||||
.annotate(weighted=F("rating") * F("rating_count") / total_ratings)
|
||||
|
@ -33,11 +37,15 @@ def get_book_superlatives():
|
|||
models.Work.objects.annotate(
|
||||
deviation=StdDev(
|
||||
"editions__review__rating",
|
||||
filter=Q(editions__review__local=True, editions__review__deleted=False),
|
||||
filter=Q(
|
||||
editions__review__user__local=True, editions__review__deleted=False
|
||||
),
|
||||
),
|
||||
rating_count=Count(
|
||||
"editions__review",
|
||||
filter=Q(editions__review__local=True, editions__review__deleted=False),
|
||||
filter=Q(
|
||||
editions__review__user__local=True, editions__review__deleted=False
|
||||
),
|
||||
),
|
||||
)
|
||||
.annotate(weighted=F("deviation") * F("rating_count") / total_ratings)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
""" Filters and tags related to shelving books """
|
||||
from django import template
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.utils import cache
|
||||
|
@ -32,6 +33,24 @@ def get_next_shelf(current_shelf):
|
|||
return "to-read"
|
||||
|
||||
|
||||
@register.filter(name="translate_shelf_name")
|
||||
def get_translated_shelf_name(shelf):
|
||||
"""produced translated shelf nidentifierame"""
|
||||
if not shelf:
|
||||
return ""
|
||||
# support obj or dict
|
||||
identifier = shelf["identifier"] if isinstance(shelf, dict) else shelf.identifier
|
||||
if identifier == "all":
|
||||
return _("All books")
|
||||
if identifier == "to-read":
|
||||
return _("To Read")
|
||||
if identifier == "reading":
|
||||
return _("Currently Reading")
|
||||
if identifier == "read":
|
||||
return _("Read")
|
||||
return shelf["name"] if isinstance(shelf, dict) else shelf.name
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def active_shelf(context, book):
|
||||
"""check what shelf a user has a book on, if any"""
|
||||
|
|
86
bookwyrm/tests/test_context_processors.py
Normal file
86
bookwyrm/tests/test_context_processors.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
""" test for context processor """
|
||||
from unittest.mock import patch
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.context_processors import site_settings
|
||||
|
||||
|
||||
class ContextProcessor(TestCase):
|
||||
"""pages you land on without really trying"""
|
||||
|
||||
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",
|
||||
)
|
||||
self.anonymous_user = AnonymousUser
|
||||
self.anonymous_user.is_authenticated = False
|
||||
self.site = models.SiteSettings.objects.create()
|
||||
|
||||
def test_theme_unset(self):
|
||||
"""logged in user, no selected theme"""
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
settings = site_settings(request)
|
||||
self.assertEqual(settings["site_theme"], "css/themes/bookwyrm-light.scss")
|
||||
|
||||
def test_theme_unset_logged_out(self):
|
||||
"""logged out user, no selected theme"""
|
||||
request = self.factory.get("")
|
||||
request.user = self.anonymous_user
|
||||
settings = site_settings(request)
|
||||
self.assertEqual(settings["site_theme"], "css/themes/bookwyrm-light.scss")
|
||||
|
||||
def test_theme_instance_default(self):
|
||||
"""logged in user, instance default theme"""
|
||||
self.site.default_theme = models.Theme.objects.get(name="BookWyrm Dark")
|
||||
self.site.save()
|
||||
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
settings = site_settings(request)
|
||||
self.assertEqual(settings["site_theme"], "css/themes/bookwyrm-dark.scss")
|
||||
|
||||
def test_theme_instance_default_logged_out(self):
|
||||
"""logged out user, instance default theme"""
|
||||
self.site.default_theme = models.Theme.objects.get(name="BookWyrm Dark")
|
||||
self.site.save()
|
||||
|
||||
request = self.factory.get("")
|
||||
request.user = self.anonymous_user
|
||||
settings = site_settings(request)
|
||||
self.assertEqual(settings["site_theme"], "css/themes/bookwyrm-dark.scss")
|
||||
|
||||
def test_theme_user_set(self):
|
||||
"""logged in user, user theme"""
|
||||
self.local_user.theme = models.Theme.objects.get(name="BookWyrm Dark")
|
||||
self.local_user.save(broadcast=False, update_fields=["theme"])
|
||||
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
settings = site_settings(request)
|
||||
self.assertEqual(settings["site_theme"], "css/themes/bookwyrm-dark.scss")
|
||||
|
||||
def test_theme_user_set_instance_default(self):
|
||||
"""logged in user, instance default theme"""
|
||||
self.site.default_theme = models.Theme.objects.get(name="BookWyrm Dark")
|
||||
self.site.save()
|
||||
|
||||
self.local_user.theme = models.Theme.objects.get(name="BookWyrm Light")
|
||||
self.local_user.save(broadcast=False, update_fields=["theme"])
|
||||
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
settings = site_settings(request)
|
||||
self.assertEqual(settings["site_theme"], "css/themes/bookwyrm-light.scss")
|
|
@ -86,6 +86,12 @@ 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/themes/?$", views.Themes.as_view(), name="settings-themes"),
|
||||
re_path(
|
||||
r"^settings/themes/(?P<theme_id>\d+)/delete/?$",
|
||||
views.delete_theme,
|
||||
name="settings-themes-delete",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/announcements/?$",
|
||||
views.Announcements.as_view(),
|
||||
|
@ -144,6 +150,11 @@ urlpatterns = [
|
|||
views.unblock_server,
|
||||
name="settings-federated-server-unblock",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/federation/(?P<server>\d+)/refresh/?$",
|
||||
views.refresh_server,
|
||||
name="settings-federated-server-refresh",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/federation/add/?$",
|
||||
views.AddFederatedServer.as_view(),
|
||||
|
|
|
@ -6,7 +6,7 @@ from .admin.automod import AutoMod, automod_delete, run_automod
|
|||
from .admin.dashboard import Dashboard
|
||||
from .admin.federation import Federation, FederatedServer
|
||||
from .admin.federation import AddFederatedServer, ImportServerBlocklist
|
||||
from .admin.federation import block_server, unblock_server
|
||||
from .admin.federation import block_server, unblock_server, refresh_server
|
||||
from .admin.email_blocklist import EmailBlocklist
|
||||
from .admin.ip_blocklist import IPBlocklist
|
||||
from .admin.invite import ManageInvites, Invite, InviteRequest
|
||||
|
@ -21,6 +21,7 @@ from .admin.reports import (
|
|||
moderator_delete_user,
|
||||
)
|
||||
from .admin.site import Site
|
||||
from .admin.themes import Themes, delete_theme
|
||||
from .admin.user_admin import UserAdmin, UserAdminList
|
||||
|
||||
# user preferences
|
||||
|
|
|
@ -11,6 +11,7 @@ from django.views.decorators.http import require_POST
|
|||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.models.user import get_or_create_remote_server
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
|
@ -37,6 +38,12 @@ class Federation(View):
|
|||
page = paginated.get_page(request.GET.get("page"))
|
||||
|
||||
data = {
|
||||
"federated_count": models.FederatedServer.objects.filter(
|
||||
status="federated"
|
||||
).count(),
|
||||
"blocked_count": models.FederatedServer.objects.filter(
|
||||
status="blocked"
|
||||
).count(),
|
||||
"servers": page,
|
||||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
|
@ -157,3 +164,14 @@ def unblock_server(request, server):
|
|||
server = get_object_or_404(models.FederatedServer, id=server)
|
||||
server.unblock()
|
||||
return redirect("settings-federated-server", server.id)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.control_federation", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def refresh_server(request, server):
|
||||
"""unblock a server"""
|
||||
server = get_object_or_404(models.FederatedServer, id=server)
|
||||
get_or_create_remote_server(server.server_name, refresh=True)
|
||||
return redirect("settings-federated-server", server.id)
|
||||
|
|
|
@ -29,7 +29,7 @@ class Site(View):
|
|||
if not form.is_valid():
|
||||
data = {"site_form": form}
|
||||
return TemplateResponse(request, "settings/site.html", data)
|
||||
form.save()
|
||||
site = form.save()
|
||||
|
||||
data = {"site_form": forms.SiteForm(instance=site), "success": True}
|
||||
return TemplateResponse(request, "settings/site.html", data)
|
||||
|
|
59
bookwyrm/views/admin/themes.py
Normal file
59
bookwyrm/views/admin/themes.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
""" manage themes """
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.contrib.staticfiles.utils import get_files
|
||||
from django.contrib.staticfiles.storage import StaticFilesStorage
|
||||
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 forms, models
|
||||
|
||||
|
||||
# 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 Themes(View):
|
||||
"""manage things like the instance name"""
|
||||
|
||||
def get(self, request):
|
||||
"""view existing themes and set defaults"""
|
||||
return TemplateResponse(request, "settings/themes.html", get_view_data())
|
||||
|
||||
def post(self, request):
|
||||
"""edit the site settings"""
|
||||
form = forms.ThemeForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
|
||||
data = get_view_data()
|
||||
|
||||
if not form.is_valid():
|
||||
data["theme_form"] = form
|
||||
else:
|
||||
data["success"] = True
|
||||
return TemplateResponse(request, "settings/themes.html", data)
|
||||
|
||||
|
||||
def get_view_data():
|
||||
"""data for view"""
|
||||
choices = list(get_files(StaticFilesStorage(), location="css/themes"))
|
||||
current = models.Theme.objects.values_list("path", flat=True)
|
||||
return {
|
||||
"themes": models.Theme.objects.all(),
|
||||
"choices": [c for c in choices if c not in current and c[-5:] == ".scss"],
|
||||
"theme_form": forms.ThemeForm(),
|
||||
}
|
||||
|
||||
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def delete_theme(request, theme_id):
|
||||
"""Remove a theme"""
|
||||
get_object_or_404(models.Theme, id=theme_id).delete()
|
||||
return redirect("settings-themes")
|
|
@ -1,7 +1,7 @@
|
|||
""" the good people stuff! the authors! """
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
from django.db.models import Avg, Q
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
@ -28,7 +28,8 @@ class Author(View):
|
|||
|
||||
books = (
|
||||
models.Work.objects.filter(Q(authors=author) | Q(editions__authors=author))
|
||||
.order_by("-published_date")
|
||||
.annotate(Avg("editions__review__rating"))
|
||||
.order_by("editions__review__rating__avg")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
|
|
@ -83,6 +83,7 @@ class Book(View):
|
|||
}
|
||||
|
||||
if request.user.is_authenticated:
|
||||
data["list_options"] = request.user.list_set.exclude(id__in=data["lists"])
|
||||
data["file_link_form"] = forms.FileLinkForm()
|
||||
readthroughs = models.ReadThrough.objects.filter(
|
||||
user=request.user,
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
""" book list views"""
|
||||
from typing import Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db import transaction
|
||||
from django.db.models import Avg, DecimalField, Q, Max
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.http import HttpResponseBadRequest, HttpResponse
|
||||
|
@ -26,7 +25,7 @@ from bookwyrm.views.helpers import is_api_request
|
|||
class List(View):
|
||||
"""book list page"""
|
||||
|
||||
def get(self, request, list_id):
|
||||
def get(self, request, list_id, add_failed=False, add_succeeded=False):
|
||||
"""display a book list"""
|
||||
book_list = get_object_or_404(models.List, id=list_id)
|
||||
book_list.raise_visible_to_user(request.user)
|
||||
|
@ -37,33 +36,10 @@ class List(View):
|
|||
query = request.GET.get("q")
|
||||
suggestions = None
|
||||
|
||||
# sort_by shall be "order" unless a valid alternative is given
|
||||
sort_by = request.GET.get("sort_by", "order")
|
||||
if sort_by not in ("order", "title", "rating"):
|
||||
sort_by = "order"
|
||||
|
||||
# direction shall be "ascending" unless a valid alternative is given
|
||||
direction = request.GET.get("direction", "ascending")
|
||||
if direction not in ("ascending", "descending"):
|
||||
direction = "ascending"
|
||||
|
||||
directional_sort_by = {
|
||||
"order": "order",
|
||||
"title": "book__title",
|
||||
"rating": "average_rating",
|
||||
}[sort_by]
|
||||
if direction == "descending":
|
||||
directional_sort_by = "-" + directional_sort_by
|
||||
|
||||
items = book_list.listitem_set.prefetch_related("user", "book", "book__authors")
|
||||
if sort_by == "rating":
|
||||
items = items.annotate(
|
||||
average_rating=Avg(
|
||||
Coalesce("book__review__rating", 0.0),
|
||||
output_field=DecimalField(),
|
||||
)
|
||||
)
|
||||
items = items.filter(approved=True).order_by(directional_sort_by)
|
||||
items = book_list.listitem_set.filter(approved=True).prefetch_related(
|
||||
"user", "book", "book__authors"
|
||||
)
|
||||
items = sort_list(request, items)
|
||||
|
||||
paginated = Paginator(items, PAGE_LENGTH)
|
||||
|
||||
|
@ -106,10 +82,10 @@ class List(View):
|
|||
"suggested_books": suggestions,
|
||||
"list_form": forms.ListForm(instance=book_list),
|
||||
"query": query or "",
|
||||
"sort_form": forms.SortListForm(
|
||||
{"direction": direction, "sort_by": sort_by}
|
||||
),
|
||||
"sort_form": forms.SortListForm(request.GET),
|
||||
"embed_url": embed_url,
|
||||
"add_failed": add_failed,
|
||||
"add_succeeded": add_succeeded,
|
||||
}
|
||||
return TemplateResponse(request, "lists/list.html", data)
|
||||
|
||||
|
@ -131,6 +107,36 @@ class List(View):
|
|||
return redirect(book_list.local_path)
|
||||
|
||||
|
||||
def sort_list(request, items):
|
||||
"""helper to handle the surprisngly involved sorting"""
|
||||
# sort_by shall be "order" unless a valid alternative is given
|
||||
sort_by = request.GET.get("sort_by", "order")
|
||||
if sort_by not in ("order", "title", "rating"):
|
||||
sort_by = "order"
|
||||
|
||||
# direction shall be "ascending" unless a valid alternative is given
|
||||
direction = request.GET.get("direction", "ascending")
|
||||
if direction not in ("ascending", "descending"):
|
||||
direction = "ascending"
|
||||
|
||||
directional_sort_by = {
|
||||
"order": "order",
|
||||
"title": "book__title",
|
||||
"rating": "average_rating",
|
||||
}[sort_by]
|
||||
if direction == "descending":
|
||||
directional_sort_by = "-" + directional_sort_by
|
||||
|
||||
if sort_by == "rating":
|
||||
items = items.annotate(
|
||||
average_rating=Avg(
|
||||
Coalesce("book__review__rating", 0.0),
|
||||
output_field=DecimalField(),
|
||||
)
|
||||
)
|
||||
return items.order_by(directional_sort_by)
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
def save_list(request, list_id):
|
||||
|
@ -179,8 +185,8 @@ def add_book(request):
|
|||
|
||||
form = forms.ListItemForm(request.POST)
|
||||
if not form.is_valid():
|
||||
# this shouldn't happen, there aren't validated fields
|
||||
raise Exception(form.errors)
|
||||
return List().get(request, book_list.id, add_failed=True)
|
||||
|
||||
item = form.save(commit=False)
|
||||
|
||||
if book_list.curation == "curated":
|
||||
|
@ -196,17 +202,9 @@ def add_book(request):
|
|||
) or 0
|
||||
increment_order_in_reverse(book_list.id, order_max + 1)
|
||||
item.order = order_max + 1
|
||||
item.save()
|
||||
|
||||
try:
|
||||
item.save()
|
||||
except IntegrityError:
|
||||
# if the book is already on the list, don't flip out
|
||||
pass
|
||||
|
||||
path = reverse("list", args=[book_list.id])
|
||||
params = request.GET.copy()
|
||||
params["updated"] = True
|
||||
return redirect(f"{path}?{urlencode(params)}")
|
||||
return List().get(request, book_list.id, add_succeeded=True)
|
||||
|
||||
|
||||
@require_POST
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
""" non-interactive pages """
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q, Count
|
||||
from django.http import Http404
|
||||
|
@ -105,6 +106,9 @@ class Followers(View):
|
|||
if is_api_request(request):
|
||||
return ActivitypubResponse(user.to_followers_activity(**request.GET))
|
||||
|
||||
if user.hide_follows:
|
||||
raise PermissionDenied()
|
||||
|
||||
followers = annotate_if_follows(request.user, user.followers)
|
||||
paginated = Paginator(followers.all(), PAGE_LENGTH)
|
||||
data = {
|
||||
|
@ -125,6 +129,9 @@ class Following(View):
|
|||
if is_api_request(request):
|
||||
return ActivitypubResponse(user.to_following_activity(**request.GET))
|
||||
|
||||
if user.hide_follows:
|
||||
raise PermissionDenied()
|
||||
|
||||
following = annotate_if_follows(request.user, user.following)
|
||||
paginated = Paginator(following.all(), PAGE_LENGTH)
|
||||
data = {
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue