Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2022-03-01 12:20:27 -08:00
commit dfba63f977
83 changed files with 5345 additions and 2145 deletions

View file

@ -1,3 +0,0 @@
@charset "utf-8";
// Copy this file to bookwyrm/static/css/ and set your instance custom styles.

3
.gitignore vendored
View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -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 = []

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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";

View file

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

View file

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

View file

@ -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,6 +66,7 @@ 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) { for (let panel of this.panels) {
if (panel.getAttribute("id") !== selectedPanelId) { if (panel.getAttribute("id") !== selectedPanelId) {
panel.setAttribute("hidden", ""); panel.setAttribute("hidden", "");
@ -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();
} }
} }
@ -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);
} }

View file

@ -14,7 +14,8 @@
{% 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">
<div class="content">
<h2> <h2>
{% blocktrans with site_name=site.name %}Welcome to {{ site_name }}!{% endblocktrans %} {% blocktrans with site_name=site.name %}Welcome to {{ site_name }}!{% endblocktrans %}
</h2> </h2>
@ -25,12 +26,13 @@
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' %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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">
<span class="icon icon-x" aria-hidden="true"></span>
<span>
{% trans "That book is already on this list." %}
</span>
</div>
{% elif add_succeeded %}
<div class="notification is-success is-light">
<span class="icon icon-check" aria-hidden="true"></span>
<span>
{% if list.curation != "open" and request.user != list.user and not list.group|is_member:request.user %} {% 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!" %} {% trans "You successfully suggested a book for this list!" %}
{% else %} {% else %}
{% trans "You successfully added a book to this list!" %} {% trans "You successfully added a book to this list!" %}
{% endif %} {% endif %}
</span>
</div> </div>
{% endif %} {% endif %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,9 +73,11 @@
<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">
<h3 class="title is-5">{% trans "Images" %}</h3>
<div class="block is-flex">
<div> <div>
<label class="label" for="id_logo">{% trans "Logo:" %}</label> <label class="label" for="id_logo">{% trans "Logo:" %}</label>
{{ site_form.logo }} {{ site_form.logo }}
@ -84,6 +91,17 @@
{{ site_form.favicon }} {{ site_form.favicon }}
</div> </div>
</div> </div>
<h3 class="title is-5">{% trans "Themes" %}</h3>
<div class="block">
<label class="label" for="id_default_theme">
{% trans "Default theme:" %}
</label>
<div class="select">
{{ site_form.default_theme }}
</div>
</div>
</div>
</section> </section>
<hr aria-hidden="true"> <hr aria-hidden="true">

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,6 +28,11 @@
{% elif request.user.is_authenticated %} {% elif request.user.is_authenticated %}
{% if user.hide_follows %}
{% if request.user in user.following.all %}
{% trans "Follows you" %}
{% endif %}
{% else %}
{% mutuals_count user as mutuals %} {% mutuals_count user as mutuals %}
<a href="{% url 'user-followers' user|username %}"> <a href="{% url 'user-followers' user|username %}">
{% if mutuals %} {% if mutuals %}
@ -38,6 +43,7 @@
{% trans "No followers you follow" %} {% trans "No followers you follow" %}
{% endif %} {% endif %}
</a> </a>
{% endif %}
{% endif %} {% endif %}
</p> </p>

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -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"
# 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 = sort_list(request, items)
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
try:
item.save() item.save()
except IntegrityError:
# if the book is already on the list, don't flip out
pass
path = reverse("list", args=[book_list.id]) return List().get(request, book_list.id, add_succeeded=True)
params = request.GET.copy()
params["updated"] = True
return redirect(f"{path}?{urlencode(params)}")
@require_POST @require_POST

View file

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