diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 7ae4e446..81061f96 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -4,6 +4,8 @@ from collections import defaultdict from urllib.parse import urlparse from django import forms +from django.contrib.staticfiles.utils import get_files +from django.contrib.staticfiles.storage import StaticFilesStorage from django.forms import ModelForm, PasswordInput, widgets, ChoiceField from django.forms.widgets import Textarea from django.utils import timezone @@ -454,6 +456,31 @@ class SiteForm(CustomForm): } +class SiteThemeForm(CustomForm): + class Meta: + model = models.SiteSettings + fields = ["default_theme"] + + +def get_theme_choices(): + """static files""" + choices = list(get_files(StaticFilesStorage(), location="css/themes")) + current = models.Theme.objects.values_list("path", flat=True) + return [(c, c) for c in choices if c not in current and c[-5:] == ".scss"] + + +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"}, choices=get_theme_choices() + ), + } + + class AnnouncementForm(CustomForm): class Meta: model = models.Announcement diff --git a/bookwyrm/migrations/0142_auto_20220227_1752.py b/bookwyrm/migrations/0142_auto_20220227_1752.py new file mode 100644 index 00000000..2282679d --- /dev/null +++ b/bookwyrm/migrations/0142_auto_20220227_1752.py @@ -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 + ), + ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 440d18d9..a8a84f09 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -26,7 +26,7 @@ from .group import Group, GroupMember, GroupMemberInvitation from .import_job import ImportJob, ImportItem -from .site import SiteSettings, SiteInvite +from .site import SiteSettings, Theme, SiteInvite from .site import PasswordReset, InviteRequest from .announcement import Announcement from .antispam import EmailBlocklist, IPBlocklist, AutoMod, automod_task diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index a40d295b..c6c53f76 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -24,6 +24,9 @@ class SiteSettings(models.Model): ) instance_description = models.TextField(default="This instance has no description.") instance_short_description = models.CharField(max_length=255, blank=True, null=True) + default_theme = models.ForeignKey( + "Theme", null=True, blank=True, on_delete=models.SET_NULL + ) # admin setup options install_mode = models.BooleanField(default=False) @@ -104,6 +107,18 @@ class SiteSettings(models.Model): super().save(*args, **kwargs) +class Theme(models.Model): + """Theme files""" + + created_date = models.DateTimeField(auto_now_add=True) + name = models.CharField(max_length=50, unique=True) + path = models.CharField(max_length=50, unique=True) + + def __str__(self): + # pylint: disable=invalid-str-returned + return self.name + + class SiteInvite(models.Model): """gives someone access to create an account on the instance""" diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 6367dcae..1198717e 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -136,6 +136,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): updated_date = models.DateTimeField(auto_now=True) last_active_date = models.DateTimeField(default=timezone.now) manually_approves_followers = fields.BooleanField(default=False) + theme = models.ForeignKey("Theme", null=True, blank=True, on_delete=models.SET_NULL) # options to turn features on and off show_goal = models.BooleanField(default=True) @@ -172,6 +173,17 @@ class User(OrderedCollectionPageMixin, AbstractUser): property_fields = [("following_link", "following")] field_tracker = FieldTracker(fields=["name", "avatar"]) + @property + def get_theme(self): + """get the theme given the user/site""" + if self.theme: + return self.theme.path + site_model = apps.get_model("bookwyrm", "SiteSettings", require_ready=True) + site = site_model.objects.get() + if site.default_theme: + return site.default_theme.path + return "css/themes/bookwyrm-light.scss" + @property def confirmation_link(self): """helper for generating confirmation links""" diff --git a/bookwyrm/static/css/bookwyrm.scss b/bookwyrm/static/css/bookwyrm.scss index 6b5e7e6b..ee25b728 100644 --- a/bookwyrm/static/css/bookwyrm.scss +++ b/bookwyrm/static/css/bookwyrm.scss @@ -1,7 +1,6 @@ @charset "utf-8"; @import "instance-settings"; -@import "themes/light.scss"; @import "vendor/bulma/bulma.sass"; @import "vendor/icons.css"; @import "bookwyrm/all.scss"; diff --git a/bookwyrm/static/css/themes/bookwyrm-dark.scss b/bookwyrm/static/css/themes/bookwyrm-dark.scss index c24e153e..1230c726 100644 --- a/bookwyrm/static/css/themes/bookwyrm-dark.scss +++ b/bookwyrm/static/css/themes/bookwyrm-dark.scss @@ -1,5 +1,6 @@ @import "../vendor/bulma/sass/utilities/initial-variables.sass"; + /* Colors ******************************************************************************/ @@ -77,3 +78,6 @@ $progress-value-background-color: $border-light; ******************************************************************************/ $family-primary: $family-sans-serif; $family-secondary: $family-sans-serif; + + +@import "../bookwyrm.scss"; \ No newline at end of file diff --git a/bookwyrm/static/css/themes/bookwyrm-light.scss b/bookwyrm/static/css/themes/bookwyrm-light.scss index 60781ff5..91cad72e 100644 --- a/bookwyrm/static/css/themes/bookwyrm-light.scss +++ b/bookwyrm/static/css/themes/bookwyrm-light.scss @@ -54,3 +54,6 @@ $invisible-overlay-background-color: rgba($scheme-invert, 0.66); ******************************************************************************/ $family-primary: $family-sans-serif; $family-secondary: $family-sans-serif; + + +@import "../bookwyrm.scss"; \ No newline at end of file diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index 0e874072..aaf21717 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -8,7 +8,9 @@ {% block title %}BookWyrm{% endblock %} - {{ site.name }} - + {% with theme_path=user.get_theme %} + + {% endwith %} diff --git a/bookwyrm/templates/settings/layout.html b/bookwyrm/templates/settings/layout.html index c5a3c5af..d76c954d 100644 --- a/bookwyrm/templates/settings/layout.html +++ b/bookwyrm/templates/settings/layout.html @@ -86,6 +86,10 @@ {% trans "Site Settings" %} {% block site-subtabs %}{% endblock %} +
  • + {% url 'settings-themes' as url %} + {% trans "Themes" %} +
  • {% endif %} diff --git a/bookwyrm/templates/settings/site.html b/bookwyrm/templates/settings/site.html index 2ecd988e..4d9dbe40 100644 --- a/bookwyrm/templates/settings/site.html +++ b/bookwyrm/templates/settings/site.html @@ -8,7 +8,7 @@ {% block site-subtabs %} @@ -33,7 +33,12 @@ {% endif %} -
    + {% csrf_token %}

    {% trans "Instance Info" %}

    @@ -68,20 +73,33 @@ -
    -

    {% trans "Images" %}

    -
    -
    - - {{ site_form.logo }} +
    +

    {% trans "Display" %}

    +
    +

    {% trans "Images" %}

    +
    +
    + + {{ site_form.logo }} +
    +
    + + {{ site_form.logo_small }} +
    +
    + + {{ site_form.favicon }} +
    -
    - - {{ site_form.logo_small }} -
    -
    - - {{ site_form.favicon }} + +

    {% trans "Themes" %}

    +
    + +
    + {{ site_form.default_theme }} +
    diff --git a/bookwyrm/templates/settings/themes.html b/bookwyrm/templates/settings/themes.html new file mode 100644 index 00000000..a0619369 --- /dev/null +++ b/bookwyrm/templates/settings/themes.html @@ -0,0 +1,112 @@ +{% extends 'settings/layout.html' %} +{% load i18n %} + +{% block title %}{% trans "Themes" %}{% endblock %} + +{% block header %}{% trans "Themes" %}{% endblock %} + +{% block panel %} +{% if success %} +
    + + + {% trans "Successfully added theme" %} + +
    +{% endif %} + +
    +
    +

    {% trans "How to add a theme" %}

    +
      +
    1. + {% trans "Copy the theme file into the bookwyrm/static/css/themes directory on your server from the command line." %} +
    2. +
    3. + {% trans "Run ./bw-dev compilescss." %} +
    4. +
    5. + {% trans "Add the file name using the form below to make it available in the application interface." %} +
    6. +
    +
    +
    + +
    +

    {% trans "Add theme" %}

    + + {% if theme_form.errors %} +
    + + + {% trans "Unable to save theme" %} + +
    + {% endif %} + + + {% csrf_token %} +
    +
    + +
    + {{ theme_form.name }} + {% include 'snippets/form_errors.html' with errors_list=theme_form.name.errors id="desc_name" %} +
    +
    + +
    + +
    +
    + {{ theme_form.path }} +
    + {% include 'snippets/form_errors.html' with errors_list=theme_form.path.errors id="desc_path" %} +
    +
    +
    + + + +
    + +
    +

    {% trans "Available Themes" %}

    +
    + + + + + + + {% for theme in themes %} + + + + + + {% endfor %} +
    + {% trans "Theme name" %} + + {% trans "File" %} + + {% trans "Actions" %} +
    {{ theme.name }}{{ theme.path }} +
    + +
    +
    +
    +
    + +{% endblock %} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index d2caa76e..bef6786d 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -86,6 +86,7 @@ urlpatterns = [ r"^settings/dashboard/?$", views.Dashboard.as_view(), name="settings-dashboard" ), re_path(r"^settings/site-settings/?$", views.Site.as_view(), name="settings-site"), + re_path(r"^settings/themes/?$", views.Themes.as_view(), name="settings-themes"), re_path( r"^settings/announcements/?$", views.Announcements.as_view(), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 76e9ff02..675221cb 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -21,6 +21,7 @@ from .admin.reports import ( moderator_delete_user, ) from .admin.site import Site +from .admin.themes import Themes from .admin.user_admin import UserAdmin, UserAdminList # user preferences diff --git a/bookwyrm/views/admin/site.py b/bookwyrm/views/admin/site.py index 7e75a820..f345d997 100644 --- a/bookwyrm/views/admin/site.py +++ b/bookwyrm/views/admin/site.py @@ -29,7 +29,7 @@ class Site(View): if not form.is_valid(): data = {"site_form": form} return TemplateResponse(request, "settings/site.html", data) - form.save() + site = form.save() data = {"site_form": forms.SiteForm(instance=site), "success": True} return TemplateResponse(request, "settings/site.html", data) diff --git a/bookwyrm/views/admin/themes.py b/bookwyrm/views/admin/themes.py new file mode 100644 index 00000000..cf11bc6d --- /dev/null +++ b/bookwyrm/views/admin/themes.py @@ -0,0 +1,38 @@ +""" manage themes """ +from django.contrib.auth.decorators import login_required, permission_required +from django.template.response import TemplateResponse +from django.utils.decorators import method_decorator +from django.views import View + +from bookwyrm import 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""" + data = { + "themes": models.Theme.objects.all(), + "theme_form": forms.ThemeForm(), + } + return TemplateResponse(request, "settings/themes.html", data) + + def post(self, request): + """edit the site settings""" + form = forms.ThemeForm(request.POST, request.FILES) + data = { + "themes": models.Theme.objects.all(), + "theme_form": form, + } + if form.is_valid(): + form.save() + data["success"] = True + data["theme_form"] = forms.ThemeForm() + return TemplateResponse(request, "settings/themes.html", data)