Merge branch 'themes' into dark-theme

This commit is contained in:
Mouse Reeve 2022-02-27 10:54:15 -08:00 committed by GitHub
commit 005b69177c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 323 additions and 19 deletions

View file

@ -4,6 +4,8 @@ from collections import defaultdict
from urllib.parse import urlparse from urllib.parse import urlparse
from django import forms 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 import ModelForm, PasswordInput, widgets, ChoiceField
from django.forms.widgets import Textarea from django.forms.widgets import Textarea
from django.utils import timezone 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 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

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

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

@ -136,6 +136,7 @@ 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)
# 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)
@ -172,6 +173,17 @@ class User(OrderedCollectionPageMixin, AbstractUser):
property_fields = [("following_link", "following")] property_fields = [("following_link", "following")]
field_tracker = FieldTracker(fields=["name", "avatar"]) 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 @property
def confirmation_link(self): def confirmation_link(self):
"""helper for generating confirmation links""" """helper for generating confirmation links"""

View file

@ -1,7 +1,6 @@
@charset "utf-8"; @charset "utf-8";
@import "instance-settings"; @import "instance-settings";
@import "themes/light.scss";
@import "vendor/bulma/bulma.sass"; @import "vendor/bulma/bulma.sass";
@import "vendor/icons.css"; @import "vendor/icons.css";
@import "bookwyrm/all.scss"; @import "bookwyrm/all.scss";

View file

@ -1,5 +1,6 @@
@import "../vendor/bulma/sass/utilities/initial-variables.sass"; @import "../vendor/bulma/sass/utilities/initial-variables.sass";
/* Colors /* Colors
******************************************************************************/ ******************************************************************************/
@ -77,3 +78,6 @@ $progress-value-background-color: $border-light;
******************************************************************************/ ******************************************************************************/
$family-primary: $family-sans-serif; $family-primary: $family-sans-serif;
$family-secondary: $family-sans-serif; $family-secondary: $family-sans-serif;
@import "../bookwyrm.scss";

View file

@ -54,3 +54,6 @@ $invisible-overlay-background-color: rgba($scheme-invert, 0.66);
******************************************************************************/ ******************************************************************************/
$family-primary: $family-sans-serif; $family-primary: $family-sans-serif;
$family-secondary: $family-sans-serif; $family-secondary: $family-sans-serif;
@import "../bookwyrm.scss";

View file

@ -8,7 +8,9 @@
<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" /> {% with theme_path=user.get_theme %}
<link href="{% sass_src 'css/themes/bookwyrm-light.scss' %}" rel="stylesheet" type="text/css" />
{% endwith %}
<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

@ -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,112 @@
{% extends 'settings/layout.html' %}
{% load i18n %}
{% block title %}{% trans "Themes" %}{% endblock %}
{% block header %}{% trans "Themes" %}{% 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"
>
{% 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">
{{ theme_form.path }}
</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>
</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>
<button type="submit" class="button is-danger is-light">{% trans "Remove theme" %}</button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
</section>
{% endblock %}

View file

@ -86,6 +86,7 @@ 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( re_path(
r"^settings/announcements/?$", r"^settings/announcements/?$",
views.Announcements.as_view(), views.Announcements.as_view(),

View file

@ -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
from .admin.user_admin import UserAdmin, UserAdminList from .admin.user_admin import UserAdmin, UserAdminList
# user preferences # user preferences

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