forked from mirrors/bookwyrm
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
|
.env
|
||||||
/images/
|
/images/
|
||||||
bookwyrm/static/css/bookwyrm.css
|
bookwyrm/static/css/bookwyrm.css
|
||||||
bookwyrm/static/css/_instance-settings.scss
|
bookwyrm/static/css/themes/
|
||||||
|
!bookwyrm/static/css/themes/bookwyrm-*.scss
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
.coverage
|
.coverage
|
||||||
|
|
|
@ -13,7 +13,7 @@ Social reading and reviewing, decentralized with ActivityPub
|
||||||
- [Set up Bookwyrm](#set-up-bookwyrm)
|
- [Set up Bookwyrm](#set-up-bookwyrm)
|
||||||
|
|
||||||
## Joining 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.
|
You can request an invite by entering your email address at https://bookwyrm.social.
|
||||||
|
|
||||||
|
|
|
@ -39,4 +39,5 @@ class Person(ActivityObject):
|
||||||
bookwyrmUser: bool = False
|
bookwyrmUser: bool = False
|
||||||
manuallyApprovesFollowers: str = False
|
manuallyApprovesFollowers: str = False
|
||||||
discoverable: str = False
|
discoverable: str = False
|
||||||
|
hideFollows: str = False
|
||||||
type: str = "Person"
|
type: str = "Person"
|
||||||
|
|
|
@ -8,8 +8,20 @@ def site_settings(request): # pylint: disable=unused-argument
|
||||||
if not request.is_secure():
|
if not request.is_secure():
|
||||||
request_protocol = "http://"
|
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 {
|
return {
|
||||||
"site": models.SiteSettings.objects.get(),
|
"site": site,
|
||||||
|
"site_theme": theme,
|
||||||
"active_announcements": models.Announcement.active_announcements(),
|
"active_announcements": models.Announcement.active_announcements(),
|
||||||
"thumbnail_generation_enabled": settings.ENABLE_THUMBNAIL_GENERATION,
|
"thumbnail_generation_enabled": settings.ENABLE_THUMBNAIL_GENERATION,
|
||||||
"media_full_url": settings.MEDIA_FULL_URL,
|
"media_full_url": settings.MEDIA_FULL_URL,
|
||||||
|
|
|
@ -153,8 +153,10 @@ class EditUserForm(CustomForm):
|
||||||
"manually_approves_followers",
|
"manually_approves_followers",
|
||||||
"default_post_privacy",
|
"default_post_privacy",
|
||||||
"discoverable",
|
"discoverable",
|
||||||
|
"hide_follows",
|
||||||
"preferred_timezone",
|
"preferred_timezone",
|
||||||
"preferred_language",
|
"preferred_language",
|
||||||
|
"theme",
|
||||||
]
|
]
|
||||||
help_texts = {f: None for f in fields}
|
help_texts = {f: None for f in fields}
|
||||||
widgets = {
|
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 AnnouncementForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Announcement
|
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 .import_job import ImportJob, ImportItem
|
||||||
|
|
||||||
from .site import SiteSettings, SiteInvite
|
from .site import SiteSettings, Theme, SiteInvite
|
||||||
from .site import PasswordReset, InviteRequest
|
from .site import PasswordReset, InviteRequest
|
||||||
from .announcement import Announcement
|
from .announcement import Announcement
|
||||||
from .antispam import EmailBlocklist, IPBlocklist, AutoMod, automod_task
|
from .antispam import EmailBlocklist, IPBlocklist, AutoMod, automod_task
|
||||||
|
|
|
@ -8,7 +8,6 @@ from .base_model import BookWyrmModel
|
||||||
|
|
||||||
|
|
||||||
DisplayTypes = [
|
DisplayTypes = [
|
||||||
("white-ter", _("None")),
|
|
||||||
("primary-light", _("Primary")),
|
("primary-light", _("Primary")),
|
||||||
("success-light", _("Success")),
|
("success-light", _("Success")),
|
||||||
("link-light", _("Link")),
|
("link-light", _("Link")),
|
||||||
|
@ -28,11 +27,7 @@ class Announcement(BookWyrmModel):
|
||||||
end_date = models.DateTimeField(blank=True, null=True)
|
end_date = models.DateTimeField(blank=True, null=True)
|
||||||
active = models.BooleanField(default=True)
|
active = models.BooleanField(default=True)
|
||||||
display_type = models.CharField(
|
display_type = models.CharField(
|
||||||
max_length=20,
|
max_length=20, choices=DisplayTypes, null=True, blank=True
|
||||||
blank=False,
|
|
||||||
null=False,
|
|
||||||
choices=DisplayTypes,
|
|
||||||
default="white-ter",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -24,6 +24,9 @@ class SiteSettings(models.Model):
|
||||||
)
|
)
|
||||||
instance_description = models.TextField(default="This instance has no description.")
|
instance_description = models.TextField(default="This instance has no description.")
|
||||||
instance_short_description = models.CharField(max_length=255, blank=True, null=True)
|
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
|
# admin setup options
|
||||||
install_mode = models.BooleanField(default=False)
|
install_mode = models.BooleanField(default=False)
|
||||||
|
@ -104,6 +107,18 @@ class SiteSettings(models.Model):
|
||||||
super().save(*args, **kwargs)
|
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):
|
class SiteInvite(models.Model):
|
||||||
"""gives someone access to create an account on the instance"""
|
"""gives someone access to create an account on the instance"""
|
||||||
|
|
||||||
|
|
|
@ -227,7 +227,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
@classmethod
|
@classmethod
|
||||||
def privacy_filter(cls, viewer, privacy_levels=None):
|
def privacy_filter(cls, viewer, privacy_levels=None):
|
||||||
queryset = super().privacy_filter(viewer, privacy_levels=privacy_levels)
|
queryset = super().privacy_filter(viewer, privacy_levels=privacy_levels)
|
||||||
return queryset.filter(deleted=False)
|
return queryset.filter(deleted=False, user__is_active=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def direct_filter(cls, queryset, viewer):
|
def direct_filter(cls, queryset, viewer):
|
||||||
|
|
|
@ -136,6 +136,8 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
updated_date = models.DateTimeField(auto_now=True)
|
updated_date = models.DateTimeField(auto_now=True)
|
||||||
last_active_date = models.DateTimeField(default=timezone.now)
|
last_active_date = models.DateTimeField(default=timezone.now)
|
||||||
manually_approves_followers = fields.BooleanField(default=False)
|
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
|
# options to turn features on and off
|
||||||
show_goal = models.BooleanField(default=True)
|
show_goal = models.BooleanField(default=True)
|
||||||
|
@ -478,10 +480,13 @@ def set_remote_server(user_id):
|
||||||
get_remote_reviews.delay(user.outbox)
|
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"""
|
"""get info on a remote server"""
|
||||||
|
server = FederatedServer()
|
||||||
try:
|
try:
|
||||||
return FederatedServer.objects.get(server_name=domain)
|
server = FederatedServer.objects.get(server_name=domain)
|
||||||
|
if not refresh:
|
||||||
|
return server
|
||||||
except FederatedServer.DoesNotExist:
|
except FederatedServer.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -496,13 +501,15 @@ def get_or_create_remote_server(domain):
|
||||||
application_type = data.get("software", {}).get("name")
|
application_type = data.get("software", {}).get("name")
|
||||||
application_version = data.get("software", {}).get("version")
|
application_version = data.get("software", {}).get("version")
|
||||||
except ConnectorException:
|
except ConnectorException:
|
||||||
|
if server.id:
|
||||||
|
return server
|
||||||
application_type = application_version = None
|
application_type = application_version = None
|
||||||
|
|
||||||
server = FederatedServer.objects.create(
|
server.server_name = domain
|
||||||
server_name=domain,
|
server.application_type = application_type
|
||||||
application_type=application_type,
|
server.application_version = application_version
|
||||||
application_version=application_version,
|
|
||||||
)
|
server.save()
|
||||||
return server
|
return server
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
env = Env()
|
env = Env()
|
||||||
env.read_env()
|
env.read_env()
|
||||||
DOMAIN = env("DOMAIN")
|
DOMAIN = env("DOMAIN")
|
||||||
VERSION = "0.3.1"
|
VERSION = "0.3.2"
|
||||||
|
|
||||||
RELEASE_API = env(
|
RELEASE_API = env(
|
||||||
"RELEASE_API",
|
"RELEASE_API",
|
||||||
|
@ -21,7 +21,7 @@ RELEASE_API = env(
|
||||||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||||
|
|
||||||
JS_CACHE = "a60e5a55"
|
JS_CACHE = "c7154efb"
|
||||||
|
|
||||||
# email
|
# email
|
||||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
@charset "utf-8";
|
@charset "utf-8";
|
||||||
|
|
||||||
@import "instance-settings";
|
|
||||||
@import "themes/light.scss";
|
|
||||||
@import "vendor/bulma/bulma.sass";
|
@import "vendor/bulma/bulma.sass";
|
||||||
@import "vendor/icons.css";
|
|
||||||
@import "bookwyrm/all.scss";
|
@import "bookwyrm/all.scss";
|
||||||
|
|
|
@ -115,7 +115,7 @@ button .button-invisible-overlay {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba($scheme-invert, 0.66);
|
background: $invisible-overlay-background-color;
|
||||||
color: white;
|
color: white;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
|
|
|
@ -53,7 +53,7 @@ details.dropdown .dropdown-menu a:focus-visible {
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
@media only screen and (max-width: 768px) {
|
||||||
details.dropdown[open] summary.dropdown-trigger::before {
|
details.dropdown[open] summary.dropdown-trigger::before {
|
||||||
background-color: rgba($scheme-invert, 0.5);
|
background-color: $modal-background-background-color;
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
.toggle-button[aria-pressed="true"],
|
.toggle-button[aria-pressed="true"],
|
||||||
.toggle-button[aria-pressed="true"]:hover {
|
.toggle-button[aria-pressed="true"]:hover {
|
||||||
background-color: hsl(171deg, 100%, 41%);
|
background-color: $primary;
|
||||||
color: white;
|
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;
|
$background: $background-secondary;
|
||||||
$menu-item-active-background-color: $link-background;
|
$menu-item-active-background-color: $link-background;
|
||||||
|
|
||||||
|
/* misc */
|
||||||
|
$invisible-overlay-background-color: rgba($scheme-invert, 0.66);
|
||||||
|
|
||||||
/* Fonts
|
/* Fonts
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
$family-primary: $family-sans-serif;
|
$family-primary: $family-sans-serif;
|
||||||
$family-secondary: $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 */
|
/* exported TabGroup */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The content below is licensed according to the W3C Software License at
|
* The content below is licensed according to the W3C Software License at
|
||||||
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
|
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
|
||||||
* Heavily modified to web component by Zach Leatherman
|
* Heavily modified to web component by Zach Leatherman
|
||||||
* Modified back to vanilla JavaScript with support for Bulma markup and nested tabs by Ned Zimmerman
|
* Modified back to vanilla JavaScript with support for Bulma markup and nested tabs by Ned Zimmerman
|
||||||
*/
|
*/
|
||||||
class TabGroup {
|
class TabGroup {
|
||||||
constructor(container) {
|
constructor(container) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
|
@ -15,7 +15,7 @@ class TabGroup {
|
||||||
this.panels = this.container.querySelectorAll(':scope > [role="tabpanel"]');
|
this.panels = this.container.querySelectorAll(':scope > [role="tabpanel"]');
|
||||||
this.delay = this.determineDelay();
|
this.delay = this.determineDelay();
|
||||||
|
|
||||||
if(!this.tablist || !this.tabs.length || !this.panels.length) {
|
if (!this.tablist || !this.tabs.length || !this.panels.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ class TabGroup {
|
||||||
left: 37,
|
left: 37,
|
||||||
up: 38,
|
up: 38,
|
||||||
right: 39,
|
right: 39,
|
||||||
down: 40
|
down: 40,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,23 +42,21 @@ class TabGroup {
|
||||||
37: -1,
|
37: -1,
|
||||||
38: -1,
|
38: -1,
|
||||||
39: 1,
|
39: 1,
|
||||||
40: 1
|
40: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
initTabs() {
|
initTabs() {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for(let tab of this.tabs) {
|
|
||||||
|
for (let tab of this.tabs) {
|
||||||
let isSelected = tab.getAttribute("aria-selected") === "true";
|
let isSelected = tab.getAttribute("aria-selected") === "true";
|
||||||
|
|
||||||
tab.setAttribute("tabindex", isSelected ? "0" : "-1");
|
tab.setAttribute("tabindex", isSelected ? "0" : "-1");
|
||||||
|
|
||||||
tab.addEventListener('click', this.clickEventListener.bind(this));
|
tab.addEventListener("click", this.clickEventListener.bind(this));
|
||||||
tab.addEventListener('keydown', this.keydownEventListener.bind(this));
|
tab.addEventListener("keydown", this.keydownEventListener.bind(this));
|
||||||
tab.addEventListener('keyup', this.keyupEventListener.bind(this));
|
tab.addEventListener("keyup", this.keyupEventListener.bind(this));
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
tab.scrollIntoView();
|
|
||||||
}
|
|
||||||
|
|
||||||
tab.index = count++;
|
tab.index = count++;
|
||||||
}
|
}
|
||||||
|
@ -68,8 +66,9 @@ class TabGroup {
|
||||||
let selectedPanelId = this.tablist
|
let selectedPanelId = this.tablist
|
||||||
.querySelector('[role="tab"][aria-selected="true"]')
|
.querySelector('[role="tab"][aria-selected="true"]')
|
||||||
.getAttribute("aria-controls");
|
.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("hidden", "");
|
||||||
}
|
}
|
||||||
panel.setAttribute("tabindex", "0");
|
panel.setAttribute("tabindex", "0");
|
||||||
|
@ -86,16 +85,18 @@ class TabGroup {
|
||||||
|
|
||||||
// Handle keydown on tabs
|
// Handle keydown on tabs
|
||||||
keydownEventListener(event) {
|
keydownEventListener(event) {
|
||||||
var key = event.keyCode;
|
const key = event.keyCode;
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case this.keys.end:
|
case this.keys.end:
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Activate last tab
|
// Activate last tab
|
||||||
this.activateTab(this.tabs[this.tabs.length - 1]);
|
this.activateTab(this.tabs[this.tabs.length - 1]);
|
||||||
break;
|
break;
|
||||||
case this.keys.home:
|
case this.keys.home:
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Activate first tab
|
// Activate first tab
|
||||||
this.activateTab(this.tabs[0]);
|
this.activateTab(this.tabs[0]);
|
||||||
break;
|
break;
|
||||||
|
@ -111,7 +112,7 @@ class TabGroup {
|
||||||
|
|
||||||
// Handle keyup on tabs
|
// Handle keyup on tabs
|
||||||
keyupEventListener(event) {
|
keyupEventListener(event) {
|
||||||
var key = event.keyCode;
|
const key = event.keyCode;
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case this.keys.left:
|
case this.keys.left:
|
||||||
|
@ -125,17 +126,16 @@ class TabGroup {
|
||||||
// only up and down arrow should function.
|
// only up and down arrow should function.
|
||||||
// In all other cases only left and right arrow function.
|
// In all other cases only left and right arrow function.
|
||||||
determineOrientation(event) {
|
determineOrientation(event) {
|
||||||
var key = event.keyCode;
|
const key = event.keyCode;
|
||||||
var vertical = this.tablist.getAttribute('aria-orientation') == 'vertical';
|
const vertical = this.tablist.getAttribute("aria-orientation") == "vertical";
|
||||||
var proceed = false;
|
let proceed = false;
|
||||||
|
|
||||||
if (vertical) {
|
if (vertical) {
|
||||||
if (key === this.keys.up || key === this.keys.down) {
|
if (key === this.keys.up || key === this.keys.down) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
proceed = true;
|
proceed = true;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
if (key === this.keys.left || key === this.keys.right) {
|
if (key === this.keys.left || key === this.keys.right) {
|
||||||
proceed = true;
|
proceed = true;
|
||||||
}
|
}
|
||||||
|
@ -149,22 +149,21 @@ class TabGroup {
|
||||||
// Either focus the next, previous, first, or last tab
|
// Either focus the next, previous, first, or last tab
|
||||||
// depending on key pressed
|
// depending on key pressed
|
||||||
switchTabOnArrowPress(event) {
|
switchTabOnArrowPress(event) {
|
||||||
var pressed = event.keyCode;
|
const pressed = event.keyCode;
|
||||||
|
|
||||||
for (let tab of this.tabs) {
|
for (let tab of this.tabs) {
|
||||||
tab.addEventListener('focus', this.focusEventHandler.bind(this));
|
tab.addEventListener("focus", this.focusEventHandler.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.direction[pressed]) {
|
if (this.direction[pressed]) {
|
||||||
var target = event.target;
|
const target = event.target;
|
||||||
|
|
||||||
if (target.index !== undefined) {
|
if (target.index !== undefined) {
|
||||||
if (this.tabs[target.index + this.direction[pressed]]) {
|
if (this.tabs[target.index + this.direction[pressed]]) {
|
||||||
this.tabs[target.index + this.direction[pressed]].focus();
|
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();
|
this.focusLastTab();
|
||||||
}
|
} else if (pressed === this.keys.right || pressed == this.keys.down) {
|
||||||
else if (pressed === this.keys.right || pressed == this.keys.down) {
|
|
||||||
this.focusFirstTab();
|
this.focusFirstTab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,8 +171,8 @@ class TabGroup {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activates any given tab panel
|
// Activates any given tab panel
|
||||||
activateTab (tab, setFocus) {
|
activateTab(tab, setFocus) {
|
||||||
if(tab.getAttribute("role") !== "tab") {
|
if (tab.getAttribute("role") !== "tab") {
|
||||||
tab = tab.closest('[role="tab"]');
|
tab = tab.closest('[role="tab"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,19 +182,19 @@ class TabGroup {
|
||||||
this.deactivateTabs();
|
this.deactivateTabs();
|
||||||
|
|
||||||
// Remove tabindex attribute
|
// Remove tabindex attribute
|
||||||
tab.removeAttribute('tabindex');
|
tab.removeAttribute("tabindex");
|
||||||
|
|
||||||
// Set the tab as selected
|
// Set the tab as selected
|
||||||
tab.setAttribute('aria-selected', 'true');
|
tab.setAttribute("aria-selected", "true");
|
||||||
|
|
||||||
// Give the tab is-active class
|
// 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)
|
// 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
|
// Remove hidden attribute from tab panel to make it visible
|
||||||
document.getElementById(controls).removeAttribute('hidden');
|
document.getElementById(controls).removeAttribute("hidden");
|
||||||
|
|
||||||
// Set focus when required
|
// Set focus when required
|
||||||
if (setFocus) {
|
if (setFocus) {
|
||||||
|
@ -206,14 +205,14 @@ class TabGroup {
|
||||||
// Deactivate all tabs and tab panels
|
// Deactivate all tabs and tab panels
|
||||||
deactivateTabs() {
|
deactivateTabs() {
|
||||||
for (let tab of this.tabs) {
|
for (let tab of this.tabs) {
|
||||||
tab.classList.remove('is-active');
|
tab.classList.remove("is-active");
|
||||||
tab.setAttribute('tabindex', '-1');
|
tab.setAttribute("tabindex", "-1");
|
||||||
tab.setAttribute('aria-selected', 'false');
|
tab.setAttribute("aria-selected", "false");
|
||||||
tab.removeEventListener('focus', this.focusEventHandler.bind(this));
|
tab.removeEventListener("focus", this.focusEventHandler.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let panel of this.panels) {
|
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
|
// Determine whether there should be a delay
|
||||||
// when user navigates with the arrow keys
|
// when user navigates with the arrow keys
|
||||||
determineDelay() {
|
determineDelay() {
|
||||||
var hasDelay = this.tablist.hasAttribute('data-delay');
|
const hasDelay = this.tablist.hasAttribute("data-delay");
|
||||||
var delay = 0;
|
let delay = 0;
|
||||||
|
|
||||||
if (hasDelay) {
|
if (hasDelay) {
|
||||||
var delayValue = this.tablist.getAttribute('data-delay');
|
const delayValue = this.tablist.getAttribute("data-delay");
|
||||||
|
|
||||||
if (delayValue) {
|
if (delayValue) {
|
||||||
delay = delayValue;
|
delay = delayValue;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// If no value is specified, default to 300ms
|
// If no value is specified, default to 300ms
|
||||||
delay = 300;
|
delay = 300;
|
||||||
}
|
}
|
||||||
|
@ -246,7 +245,7 @@ class TabGroup {
|
||||||
}
|
}
|
||||||
|
|
||||||
focusEventHandler(event) {
|
focusEventHandler(event) {
|
||||||
var target = event.target;
|
const target = event.target;
|
||||||
|
|
||||||
setTimeout(this.checkTabFocus.bind(this), this.delay, target);
|
setTimeout(this.checkTabFocus.bind(this), this.delay, target);
|
||||||
}
|
}
|
|
@ -14,23 +14,25 @@
|
||||||
{% cache 604800 about_page %}
|
{% cache 604800 about_page %}
|
||||||
|
|
||||||
{% get_book_superlatives as superlatives %}
|
{% get_book_superlatives as superlatives %}
|
||||||
<section class="content pb-4">
|
<section class=" pb-4">
|
||||||
<h2>
|
<div class="content">
|
||||||
{% blocktrans with site_name=site.name %}Welcome to {{ site_name }}!{% endblocktrans %}
|
<h2>
|
||||||
</h2>
|
{% blocktrans with site_name=site.name %}Welcome to {{ site_name }}!{% endblocktrans %}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<p class="subtitle notification has-background-primary-highlight">
|
<p class="subtitle notification has-background-primary-highlight">
|
||||||
{% blocktrans trimmed with site_name=site.name %}
|
{% blocktrans trimmed with site_name=site.name %}
|
||||||
{{ site_name }} is part of <em>BookWyrm</em>, a network of independent, self-directed communities for readers.
|
{{ 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.
|
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 %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
{% if superlatives.top_rated %}
|
{% if superlatives.top_rated %}
|
||||||
{% with book=superlatives.top_rated.default_edition rating=superlatives.top_rated.rating %}
|
{% with book=superlatives.top_rated.default_edition rating=superlatives.top_rated.rating %}
|
||||||
<div class="column is-one-third is-flex">
|
<div class="column is-one-third is-flex">
|
||||||
<div class="media notification">
|
<div class="media notification is-clipped">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<a href="{{ book.local_path }}">
|
<a href="{{ book.local_path }}">
|
||||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %}
|
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %}
|
||||||
|
@ -49,7 +51,7 @@
|
||||||
{% if superlatives.wanted %}
|
{% if superlatives.wanted %}
|
||||||
{% with book=superlatives.wanted.default_edition %}
|
{% with book=superlatives.wanted.default_edition %}
|
||||||
<div class="column is-one-third is-flex">
|
<div class="column is-one-third is-flex">
|
||||||
<div class="media notification">
|
<div class="media notification is-clipped">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<a href="{{ book.local_path }}">
|
<a href="{{ book.local_path }}">
|
||||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %}
|
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %}
|
||||||
|
@ -68,7 +70,7 @@
|
||||||
{% if superlatives.controversial %}
|
{% if superlatives.controversial %}
|
||||||
{% with book=superlatives.controversial.default_edition %}
|
{% with book=superlatives.controversial.default_edition %}
|
||||||
<div class="column is-one-third is-flex">
|
<div class="column is-one-third is-flex">
|
||||||
<div class="media notification">
|
<div class="media notification is-clipped">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<a href="{{ book.local_path }}">
|
<a href="{{ book.local_path }}">
|
||||||
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %}
|
{% include 'snippets/book_cover.html' with book=book cover_class='is-h-m' size='medium' aria='show' %}
|
||||||
|
|
|
@ -352,7 +352,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{% if request.user.list_set.exists %}
|
{% if list_options.exists %}
|
||||||
<form name="list-add" method="post" action="{% url 'list-add-book' %}">
|
<form name="list-add" method="post" action="{% url 'list-add-book' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ book.id }}">
|
<input type="hidden" name="book" value="{{ book.id }}">
|
||||||
|
@ -361,7 +361,7 @@
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="select control is-clipped">
|
<div class="select control is-clipped">
|
||||||
<select name="book_list" id="id_list">
|
<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>
|
<option value="{{ list.id }}">{{ list.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
@ -386,6 +386,6 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% 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>
|
<script src="{% static "js/autocomplete.js" %}?v={{ js_cache }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
</div>
|
</div>
|
||||||
<footer class="card-footer">
|
<footer class="card-footer">
|
||||||
{% if user != request.user %}
|
{% if user != request.user %}
|
||||||
{% if user.mutuals %}
|
{% if user.mutuals and not user.hide_follows %}
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
<div class="has-text-centered">
|
<div class="has-text-centered">
|
||||||
<p class="title is-6 mb-0">{{ user.mutuals }}</p>
|
<p class="title is-6 mb-0">{{ user.mutuals }}</p>
|
||||||
|
|
|
@ -31,5 +31,5 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{% static "js/vendor/tabs.js" %}?v={{ js_cache }}"></script>
|
<script src="{% static "js/tabs.js" %}?v={{ js_cache }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
|
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
|
||||||
</a>
|
</a>
|
||||||
{% include 'snippets/add_to_group_button.html' with user=user group=group %}
|
{% 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">
|
<p class="help">
|
||||||
{% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %}
|
{% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %}
|
||||||
{{ mutuals }} follower you follow
|
{{ mutuals }} follower you follow
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<head>
|
<head>
|
||||||
<title>{% block title %}BookWyrm{% endblock %} - {{ site.name }}</title>
|
<title>{% block title %}BookWyrm{% endblock %} - {{ site.name }}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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 %}" />
|
<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">
|
<div class="columns mt-3">
|
||||||
<section class="column is-three-quarters">
|
<section class="column is-three-quarters">
|
||||||
{% if request.GET.updated %}
|
{% if add_failed %}
|
||||||
<div class="notification is-primary">
|
<div class="notification is-danger is-light">
|
||||||
{% if list.curation != "open" and request.user != list.user and not list.group|is_member:request.user %}
|
<span class="icon icon-x" aria-hidden="true"></span>
|
||||||
{% trans "You successfully suggested a book for this list!" %}
|
<span>
|
||||||
{% else %}
|
{% trans "That book is already on this list." %}
|
||||||
{% trans "You successfully added a book to this list!" %}
|
</span>
|
||||||
{% endif %}
|
</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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
{% block profile-tabs %}
|
{% block profile-tabs %}
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li><a href="#profile">{% trans "Profile" %}</a></li>
|
<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>
|
<li><a href="#privacy">{% trans "Privacy" %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
<hr aria-hidden="true">
|
<hr aria-hidden="true">
|
||||||
|
|
||||||
<section class="block" id="display-preferences">
|
<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="box">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="checkbox label" for="id_show_goal">
|
<label class="checkbox label" for="id_show_goal">
|
||||||
|
@ -97,6 +97,12 @@
|
||||||
{{ form.preferred_language }}
|
{{ form.preferred_language }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="id_them">{% trans "Theme:" %}</label>
|
||||||
|
<div class="select">
|
||||||
|
{{ form.theme }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -111,6 +117,12 @@
|
||||||
{% trans "Manually approve followers" %}
|
{% trans "Manually approve followers" %}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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">
|
<div class="field">
|
||||||
<label class="label" for="id_default_post_privacy">
|
<label class="label" for="id_default_post_privacy">
|
||||||
{% trans "Default post privacy:" %}
|
{% trans "Default post privacy:" %}
|
||||||
|
|
|
@ -4,7 +4,19 @@
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{% trans "Add instance" %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
@ -73,9 +85,13 @@
|
||||||
<label class="label" for="id_notes">
|
<label class="label" for="id_notes">
|
||||||
{% trans "Notes:" %}
|
{% trans "Notes:" %}
|
||||||
</label>
|
</label>
|
||||||
<textarea name="notes" cols="40" rows="5" class="textarea" id="id_notes">
|
<textarea
|
||||||
{{ form.notes.value|default:'' }}
|
name="notes"
|
||||||
</textarea>
|
cols="40"
|
||||||
|
rows="5"
|
||||||
|
class="textarea"
|
||||||
|
id="id_notes"
|
||||||
|
>{{ form.notes.value|default:'' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="button is-primary">
|
<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>
|
{% 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 %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
|
@ -4,7 +4,19 @@
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{% trans "Import Blocklist" %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
|
@ -16,11 +16,11 @@
|
||||||
<ul>
|
<ul>
|
||||||
{% url 'settings-federation' status='federated' as url %}
|
{% url 'settings-federation' status='federated' as url %}
|
||||||
<li {% if request.path in url %}class="is-active" aria-current="page"{% endif %}>
|
<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>
|
</li>
|
||||||
{% url 'settings-federation' status='blocked' as url %}
|
{% url 'settings-federation' status='blocked' as url %}
|
||||||
<li {% if url in request.path %}class="is-active" aria-current="page"{% endif %}>
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -86,6 +86,10 @@
|
||||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Site Settings" %}</a>
|
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Site Settings" %}</a>
|
||||||
{% block site-subtabs %}{% endblock %}
|
{% block site-subtabs %}{% endblock %}
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
{% block site-subtabs %}
|
{% block site-subtabs %}
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li><a href="#instance-info">{% trans "Instance Info" %}</a></li>
|
<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="#footer">{% trans "Footer Content" %}</a></li>
|
||||||
<li><a href="#registration">{% trans "Registration" %}</a></li>
|
<li><a href="#registration">{% trans "Registration" %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -33,7 +33,12 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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 %}
|
{% csrf_token %}
|
||||||
<section class="block" id="instance_info">
|
<section class="block" id="instance_info">
|
||||||
<h2 class="title is-4">{% trans "Instance Info" %}</h2>
|
<h2 class="title is-4">{% trans "Instance Info" %}</h2>
|
||||||
|
@ -68,20 +73,33 @@
|
||||||
|
|
||||||
<hr aria-hidden="true">
|
<hr aria-hidden="true">
|
||||||
|
|
||||||
<section class="block" id="images">
|
<section class="block" id="display">
|
||||||
<h2 class="title is-4">{% trans "Images" %}</h2>
|
<h2 class="title is-4">{% trans "Display" %}</h2>
|
||||||
<div class="box is-flex">
|
<div class="box">
|
||||||
<div>
|
<h3 class="title is-5">{% trans "Images" %}</h3>
|
||||||
<label class="label" for="id_logo">{% trans "Logo:" %}</label>
|
<div class="block is-flex">
|
||||||
{{ site_form.logo }}
|
<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>
|
||||||
<div>
|
|
||||||
<label class="label" for="id_logo_small">{% trans "Logo small:" %}</label>
|
<h3 class="title is-5">{% trans "Themes" %}</h3>
|
||||||
{{ site_form.logo_small }}
|
<div class="block">
|
||||||
</div>
|
<label class="label" for="id_default_theme">
|
||||||
<div>
|
{% trans "Default theme:" %}
|
||||||
<label class="label" for="id_favicon">{% trans "Favicon:" %}</label>
|
</label>
|
||||||
{{ site_form.favicon }}
|
<div class="select">
|
||||||
|
{{ site_form.default_theme }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 %}
|
{% load humanize %}{% load i18n %}{% load utilities %}
|
||||||
{% with announcement.id|uuid as uuid %}
|
{% with announcement.id|uuid as uuid %}
|
||||||
<aside
|
<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 %}
|
{% if not admin_mode %}data-hide="hide_announcement_{{ announcement.id }}"{% endif %}
|
||||||
>
|
>
|
||||||
<details>
|
<details>
|
||||||
|
|
|
@ -71,7 +71,9 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ book.id }}">
|
<input type="hidden" name="book" value="{{ book.id }}">
|
||||||
<input type="hidden" name="shelf" value="{{ user_shelf.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>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
|
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
|
||||||
<input type="hidden" name="shelf" value="{{ active_shelf.shelf.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">
|
<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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -69,7 +69,6 @@
|
||||||
|
|
||||||
{% block card-bonus %}
|
{% block card-bonus %}
|
||||||
{% if request.user.is_authenticated and not moderation_mode and not no_interact %}
|
{% 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 }}">
|
<section class="reply-panel is-hidden" id="show_comment_{{ status.id }}">
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
|
@ -77,6 +76,5 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
|
<span title="@{{ user|username }}" class="is-block pb-3">@{{ user|username|truncatechars:8 }}</span>
|
||||||
</a>
|
</a>
|
||||||
{% include 'snippets/follow_button.html' with user=user minimal=True %}
|
{% include 'snippets/follow_button.html' with user=user minimal=True %}
|
||||||
{% if user.mutuals %}
|
{% if user.mutuals and not user.hide_follows %}
|
||||||
<p class="help">
|
<p class="help">
|
||||||
{% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %}
|
{% blocktrans trimmed with mutuals=user.mutuals|intcomma count counter=user.mutuals %}
|
||||||
{{ mutuals }} follower you follow
|
{{ mutuals }} follower you follow
|
||||||
|
|
|
@ -1,12 +1,4 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% if shelf.identifier == 'all' %}
|
{% load shelf_tags %}
|
||||||
{% trans "All books" %}
|
|
||||||
{% elif shelf.identifier == 'to-read' %}
|
{{ shelf|translate_shelf_name }}
|
||||||
{% trans "To Read" %}
|
|
||||||
{% elif shelf.identifier == 'reading' %}
|
|
||||||
{% trans "Currently Reading" %}
|
|
||||||
{% elif shelf.identifier == 'read' %}
|
|
||||||
{% trans "Read" %}
|
|
||||||
{% else %}
|
|
||||||
{{ shelf.name }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
|
@ -28,16 +28,22 @@
|
||||||
|
|
||||||
{% elif request.user.is_authenticated %}
|
{% elif request.user.is_authenticated %}
|
||||||
|
|
||||||
{% mutuals_count user as mutuals %}
|
{% if user.hide_follows %}
|
||||||
<a href="{% url 'user-followers' user|username %}">
|
{% if request.user in user.following.all %}
|
||||||
{% if mutuals %}
|
{% trans "Follows you" %}
|
||||||
{% 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 %}
|
{% 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 %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -16,11 +16,15 @@ def get_book_superlatives():
|
||||||
models.Work.objects.annotate(
|
models.Work.objects.annotate(
|
||||||
rating=Avg(
|
rating=Avg(
|
||||||
"editions__review__rating",
|
"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(
|
rating_count=Count(
|
||||||
"editions__review",
|
"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)
|
.annotate(weighted=F("rating") * F("rating_count") / total_ratings)
|
||||||
|
@ -33,11 +37,15 @@ def get_book_superlatives():
|
||||||
models.Work.objects.annotate(
|
models.Work.objects.annotate(
|
||||||
deviation=StdDev(
|
deviation=StdDev(
|
||||||
"editions__review__rating",
|
"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(
|
rating_count=Count(
|
||||||
"editions__review",
|
"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)
|
.annotate(weighted=F("deviation") * F("rating_count") / total_ratings)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
""" Filters and tags related to shelving books """
|
""" Filters and tags related to shelving books """
|
||||||
from django import template
|
from django import template
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.utils import cache
|
from bookwyrm.utils import cache
|
||||||
|
@ -32,6 +33,24 @@ def get_next_shelf(current_shelf):
|
||||||
return "to-read"
|
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)
|
@register.simple_tag(takes_context=True)
|
||||||
def active_shelf(context, book):
|
def active_shelf(context, book):
|
||||||
"""check what shelf a user has a book on, if any"""
|
"""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"
|
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-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(
|
re_path(
|
||||||
r"^settings/announcements/?$",
|
r"^settings/announcements/?$",
|
||||||
views.Announcements.as_view(),
|
views.Announcements.as_view(),
|
||||||
|
@ -144,6 +150,11 @@ urlpatterns = [
|
||||||
views.unblock_server,
|
views.unblock_server,
|
||||||
name="settings-federated-server-unblock",
|
name="settings-federated-server-unblock",
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
r"^settings/federation/(?P<server>\d+)/refresh/?$",
|
||||||
|
views.refresh_server,
|
||||||
|
name="settings-federated-server-refresh",
|
||||||
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/federation/add/?$",
|
r"^settings/federation/add/?$",
|
||||||
views.AddFederatedServer.as_view(),
|
views.AddFederatedServer.as_view(),
|
||||||
|
|
|
@ -6,7 +6,7 @@ from .admin.automod import AutoMod, automod_delete, run_automod
|
||||||
from .admin.dashboard import Dashboard
|
from .admin.dashboard import Dashboard
|
||||||
from .admin.federation import Federation, FederatedServer
|
from .admin.federation import Federation, FederatedServer
|
||||||
from .admin.federation import AddFederatedServer, ImportServerBlocklist
|
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.email_blocklist import EmailBlocklist
|
||||||
from .admin.ip_blocklist import IPBlocklist
|
from .admin.ip_blocklist import IPBlocklist
|
||||||
from .admin.invite import ManageInvites, Invite, InviteRequest
|
from .admin.invite import ManageInvites, Invite, InviteRequest
|
||||||
|
@ -21,6 +21,7 @@ from .admin.reports import (
|
||||||
moderator_delete_user,
|
moderator_delete_user,
|
||||||
)
|
)
|
||||||
from .admin.site import Site
|
from .admin.site import Site
|
||||||
|
from .admin.themes import Themes, delete_theme
|
||||||
from .admin.user_admin import UserAdmin, UserAdminList
|
from .admin.user_admin import UserAdmin, UserAdminList
|
||||||
|
|
||||||
# user preferences
|
# user preferences
|
||||||
|
|
|
@ -11,6 +11,7 @@ from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
from bookwyrm import forms, models
|
from bookwyrm import forms, models
|
||||||
from bookwyrm.settings import PAGE_LENGTH
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
|
from bookwyrm.models.user import get_or_create_remote_server
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable= no-self-use
|
# pylint: disable= no-self-use
|
||||||
|
@ -37,6 +38,12 @@ class Federation(View):
|
||||||
page = paginated.get_page(request.GET.get("page"))
|
page = paginated.get_page(request.GET.get("page"))
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
|
"federated_count": models.FederatedServer.objects.filter(
|
||||||
|
status="federated"
|
||||||
|
).count(),
|
||||||
|
"blocked_count": models.FederatedServer.objects.filter(
|
||||||
|
status="blocked"
|
||||||
|
).count(),
|
||||||
"servers": page,
|
"servers": page,
|
||||||
"page_range": paginated.get_elided_page_range(
|
"page_range": paginated.get_elided_page_range(
|
||||||
page.number, on_each_side=2, on_ends=1
|
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 = get_object_or_404(models.FederatedServer, id=server)
|
||||||
server.unblock()
|
server.unblock()
|
||||||
return redirect("settings-federated-server", server.id)
|
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():
|
if not form.is_valid():
|
||||||
data = {"site_form": form}
|
data = {"site_form": form}
|
||||||
return TemplateResponse(request, "settings/site.html", data)
|
return TemplateResponse(request, "settings/site.html", data)
|
||||||
form.save()
|
site = form.save()
|
||||||
|
|
||||||
data = {"site_form": forms.SiteForm(instance=site), "success": True}
|
data = {"site_form": forms.SiteForm(instance=site), "success": True}
|
||||||
return TemplateResponse(request, "settings/site.html", data)
|
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! """
|
""" the good people stuff! the authors! """
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
from django.core.paginator import Paginator
|
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.shortcuts import get_object_or_404, redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
@ -28,7 +28,8 @@ class Author(View):
|
||||||
|
|
||||||
books = (
|
books = (
|
||||||
models.Work.objects.filter(Q(authors=author) | Q(editions__authors=author))
|
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()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -83,6 +83,7 @@ class Book(View):
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
|
data["list_options"] = request.user.list_set.exclude(id__in=data["lists"])
|
||||||
data["file_link_form"] = forms.FileLinkForm()
|
data["file_link_form"] = forms.FileLinkForm()
|
||||||
readthroughs = models.ReadThrough.objects.filter(
|
readthroughs = models.ReadThrough.objects.filter(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
""" book list views"""
|
""" book list views"""
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from urllib.parse import urlencode
|
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.core.paginator import Paginator
|
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 import Avg, DecimalField, Q, Max
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.http import HttpResponseBadRequest, HttpResponse
|
from django.http import HttpResponseBadRequest, HttpResponse
|
||||||
|
@ -26,7 +25,7 @@ from bookwyrm.views.helpers import is_api_request
|
||||||
class List(View):
|
class List(View):
|
||||||
"""book list page"""
|
"""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"""
|
"""display a book list"""
|
||||||
book_list = get_object_or_404(models.List, id=list_id)
|
book_list = get_object_or_404(models.List, id=list_id)
|
||||||
book_list.raise_visible_to_user(request.user)
|
book_list.raise_visible_to_user(request.user)
|
||||||
|
@ -37,33 +36,10 @@ class List(View):
|
||||||
query = request.GET.get("q")
|
query = request.GET.get("q")
|
||||||
suggestions = None
|
suggestions = None
|
||||||
|
|
||||||
# sort_by shall be "order" unless a valid alternative is given
|
items = book_list.listitem_set.filter(approved=True).prefetch_related(
|
||||||
sort_by = request.GET.get("sort_by", "order")
|
"user", "book", "book__authors"
|
||||||
if sort_by not in ("order", "title", "rating"):
|
)
|
||||||
sort_by = "order"
|
items = sort_list(request, items)
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
paginated = Paginator(items, PAGE_LENGTH)
|
paginated = Paginator(items, PAGE_LENGTH)
|
||||||
|
|
||||||
|
@ -106,10 +82,10 @@ class List(View):
|
||||||
"suggested_books": suggestions,
|
"suggested_books": suggestions,
|
||||||
"list_form": forms.ListForm(instance=book_list),
|
"list_form": forms.ListForm(instance=book_list),
|
||||||
"query": query or "",
|
"query": query or "",
|
||||||
"sort_form": forms.SortListForm(
|
"sort_form": forms.SortListForm(request.GET),
|
||||||
{"direction": direction, "sort_by": sort_by}
|
|
||||||
),
|
|
||||||
"embed_url": embed_url,
|
"embed_url": embed_url,
|
||||||
|
"add_failed": add_failed,
|
||||||
|
"add_succeeded": add_succeeded,
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, "lists/list.html", data)
|
return TemplateResponse(request, "lists/list.html", data)
|
||||||
|
|
||||||
|
@ -131,6 +107,36 @@ class List(View):
|
||||||
return redirect(book_list.local_path)
|
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
|
@require_POST
|
||||||
@login_required
|
@login_required
|
||||||
def save_list(request, list_id):
|
def save_list(request, list_id):
|
||||||
|
@ -179,8 +185,8 @@ def add_book(request):
|
||||||
|
|
||||||
form = forms.ListItemForm(request.POST)
|
form = forms.ListItemForm(request.POST)
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
# this shouldn't happen, there aren't validated fields
|
return List().get(request, book_list.id, add_failed=True)
|
||||||
raise Exception(form.errors)
|
|
||||||
item = form.save(commit=False)
|
item = form.save(commit=False)
|
||||||
|
|
||||||
if book_list.curation == "curated":
|
if book_list.curation == "curated":
|
||||||
|
@ -196,17 +202,9 @@ def add_book(request):
|
||||||
) or 0
|
) or 0
|
||||||
increment_order_in_reverse(book_list.id, order_max + 1)
|
increment_order_in_reverse(book_list.id, order_max + 1)
|
||||||
item.order = order_max + 1
|
item.order = order_max + 1
|
||||||
|
item.save()
|
||||||
|
|
||||||
try:
|
return List().get(request, book_list.id, add_succeeded=True)
|
||||||
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)}")
|
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
""" non-interactive pages """
|
""" non-interactive pages """
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db.models import Q, Count
|
from django.db.models import Q, Count
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
@ -105,6 +106,9 @@ class Followers(View):
|
||||||
if is_api_request(request):
|
if is_api_request(request):
|
||||||
return ActivitypubResponse(user.to_followers_activity(**request.GET))
|
return ActivitypubResponse(user.to_followers_activity(**request.GET))
|
||||||
|
|
||||||
|
if user.hide_follows:
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
followers = annotate_if_follows(request.user, user.followers)
|
followers = annotate_if_follows(request.user, user.followers)
|
||||||
paginated = Paginator(followers.all(), PAGE_LENGTH)
|
paginated = Paginator(followers.all(), PAGE_LENGTH)
|
||||||
data = {
|
data = {
|
||||||
|
@ -125,6 +129,9 @@ class Following(View):
|
||||||
if is_api_request(request):
|
if is_api_request(request):
|
||||||
return ActivitypubResponse(user.to_following_activity(**request.GET))
|
return ActivitypubResponse(user.to_following_activity(**request.GET))
|
||||||
|
|
||||||
|
if user.hide_follows:
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
following = annotate_if_follows(request.user, user.following)
|
following = annotate_if_follows(request.user, user.following)
|
||||||
paginated = Paginator(following.all(), PAGE_LENGTH)
|
paginated = Paginator(following.all(), PAGE_LENGTH)
|
||||||
data = {
|
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