diff --git a/README.md b/README.md index 8cec8cf7..845b7475 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ It aims to be very light, simple to install and simple to maintain. Its main fea - OpenID Connect support; - No outdated or exotic protocol support; - No additional database required. Everything is stored in your LDAP server; +- Customizable, themable; - The code is easy to read and easy to edit, and you should! # Screenshots diff --git a/canaille/__init__.py b/canaille/__init__.py index 8415570d..410972c1 100644 --- a/canaille/__init__.py +++ b/canaille/__init__.py @@ -15,8 +15,9 @@ import canaille.account import canaille.groups import canaille.well_known -from flask import Flask, g, request, render_template, session +from flask import Flask, g, request, session from flask_babel import Babel +from flask_themer import Themer, render_template, FileSystemThemeLoader from .flaskutils import current_user from .ldaputils import LDAPObject @@ -120,6 +121,18 @@ def setup_app(app): babel = Babel(app) + additional_themes_dir = ( + os.path.dirname(app.config["THEME"]) + if app.config.get("THEME") and os.path.exists(app.config["THEME"]) + else None + ) + themer = Themer( + app, + loaders=[FileSystemThemeLoader(additional_themes_dir)] + if additional_themes_dir + else None, + ) + @babel.localeselector def get_locale(): user = getattr(g, "user", None) @@ -137,6 +150,11 @@ def setup_app(app): if user is not None: return user.timezone + @themer.current_theme_loader + def get_current_theme(): + # if config['THEME'] may be a theme name or an absolute path + return app.config.get("THEME", "default").split("/")[-1] + @app.before_request def before_request(): setup_ldap_connection(app) diff --git a/canaille/account.py b/canaille/account.py index f6f14cad..cb129d53 100644 --- a/canaille/account.py +++ b/canaille/account.py @@ -7,11 +7,11 @@ from flask import ( url_for, current_app, abort, - render_template, redirect, session, ) from flask_babel import gettext as _ +from flask_themer import render_template from werkzeug.datastructures import CombinedMultiDict, FileStorage from .forms import ( LoginForm, diff --git a/canaille/admin/authorizations.py b/canaille/admin/authorizations.py index c8a745c7..5003544d 100644 --- a/canaille/admin/authorizations.py +++ b/canaille/admin/authorizations.py @@ -1,4 +1,5 @@ -from flask import Blueprint, render_template +from flask import Blueprint +from flask_themer import render_template from canaille.models import AuthorizationCode from canaille.flaskutils import admin_needed diff --git a/canaille/admin/clients.py b/canaille/admin/clients.py index 3c473340..43ede5b9 100644 --- a/canaille/admin/clients.py +++ b/canaille/admin/clients.py @@ -1,6 +1,7 @@ import datetime import wtforms -from flask import Blueprint, render_template, request, flash, redirect, url_for, abort +from flask import Blueprint, request, flash, redirect, url_for, abort +from flask_themer import render_template from flask_wtf import FlaskForm from flask_babel import lazy_gettext as _ from werkzeug.security import gen_salt diff --git a/canaille/admin/mail.py b/canaille/admin/mail.py index 3cf39245..ecaddbdb 100644 --- a/canaille/admin/mail.py +++ b/canaille/admin/mail.py @@ -1,4 +1,5 @@ -from flask import Blueprint, render_template, current_app, url_for +from flask import Blueprint, current_app, url_for +from flask_themer import render_template from flask_babel import gettext as _ from canaille.flaskutils import admin_needed from canaille.mails import profile_hash diff --git a/canaille/admin/tokens.py b/canaille/admin/tokens.py index e4019d12..209fd648 100644 --- a/canaille/admin/tokens.py +++ b/canaille/admin/tokens.py @@ -1,4 +1,5 @@ -from flask import Blueprint, render_template +from flask import Blueprint +from flask_themer import render_template from canaille.models import Token from canaille.flaskutils import admin_needed diff --git a/canaille/conf/config.sample.toml b/canaille/conf/config.sample.toml index 9a6f93d0..c4d5d7ae 100644 --- a/canaille/conf/config.sample.toml +++ b/canaille/conf/config.sample.toml @@ -17,6 +17,11 @@ LOGO = "/static/img/canaille-head.png" # Your favicon. If unset the LOGO will be used. FAVICON = "/static/img/canaille-c.png" +# The name of a theme in the 'theme' directory, or an absolute path +# to a theme. Defaults to 'default'. Theming is done with +# https://github.com/tktech/flask-themer +# THEME = "default" + # If unset, language is detected # LANGUAGE = "en" diff --git a/canaille/configuration.py b/canaille/configuration.py index a1f64239..8316db01 100644 --- a/canaille/configuration.py +++ b/canaille/configuration.py @@ -8,12 +8,25 @@ from cryptography.hazmat.primitives import serialization as crypto_serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.backends import default_backend as crypto_default_backend +ROOT = os.path.dirname(os.path.abspath(__file__)) + class ConfigurationException(Exception): pass def validate(config, validate_remote=False): + validate_keypair(config) + validate_theme(config) + + if not validate_remote: + return + + validate_ldap_configuration(config) + validate_smtp_configuration(config) + + +def validate_keypair(config): if not os.path.exists(config["JWT"]["PUBLIC_KEY"]): raise ConfigurationException( f'Public key does not exist {config["JWT"]["PUBLIC_KEY"]}' @@ -24,12 +37,6 @@ def validate(config, validate_remote=False): f'Private key does not exist {config["JWT"]["PRIVATE_KEY"]}' ) - if not validate_remote: - return - - validate_ldap_configuration(config) - validate_smtp_configuration(config) - def validate_ldap_configuration(config): from canaille.models import User, Group @@ -127,6 +134,16 @@ def validate_smtp_configuration(config): ) from exc +def validate_theme(config): + if not config.get("THEME"): + return + + if not os.path.exists(config["THEME"]) and not os.path.exists( + os.path.join(ROOT, "themes", config["THEME"]) + ): + raise ConfigurationException(f'Cannot find theme \'{config["THEME"]}\'') + + def setup_dev_keypair(config): if os.path.exists(config["JWT"]["PUBLIC_KEY"]) or os.path.exists( config["JWT"]["PRIVATE_KEY"] diff --git a/canaille/consents.py b/canaille/consents.py index 9f82a3ab..051f3388 100644 --- a/canaille/consents.py +++ b/canaille/consents.py @@ -1,4 +1,5 @@ -from flask import Blueprint, render_template, flash, redirect, url_for +from flask import Blueprint, flash, redirect, url_for +from flask_themer import render_template from flask_babel import gettext from canaille.models import Consent, Client from canaille.flaskutils import user_needed diff --git a/canaille/groups.py b/canaille/groups.py index 0c7882f9..c90c449d 100644 --- a/canaille/groups.py +++ b/canaille/groups.py @@ -1,6 +1,5 @@ from flask import ( Blueprint, - render_template, redirect, url_for, request, @@ -9,6 +8,7 @@ from flask import ( abort, ) from flask_babel import gettext as _ +from flask_themer import render_template from .flaskutils import moderator_needed from .forms import GroupForm diff --git a/canaille/mails.py b/canaille/mails.py index 3d3f23fc..94ffcebb 100644 --- a/canaille/mails.py +++ b/canaille/mails.py @@ -1,6 +1,7 @@ import hashlib -from flask import url_for, render_template, current_app +from flask import url_for, current_app from flask_babel import gettext as _ +from flask_themer import render_template from .apputils import logo, send_email diff --git a/canaille/oauth.py b/canaille/oauth.py index 42601dbe..fa2417ee 100644 --- a/canaille/oauth.py +++ b/canaille/oauth.py @@ -9,11 +9,11 @@ from flask import ( session, redirect, abort, - render_template, jsonify, flash, ) from flask_babel import gettext, lazy_gettext as _ +from flask_themer import render_template from .models import User, Client, Consent from .oauth2utils import ( authorization, diff --git a/canaille/templates/about.html b/canaille/templates/about.html index d631471b..5f2c8ff0 100644 --- a/canaille/templates/about.html +++ b/canaille/templates/about.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends theme('base.html') %} {% import 'fomanticui.j2' as sui %} {% block content %} diff --git a/canaille/templates/admin/authorization_list.html b/canaille/templates/admin/authorization_list.html index dbf83c66..c254cd25 100644 --- a/canaille/templates/admin/authorization_list.html +++ b/canaille/templates/admin/authorization_list.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends theme('base.html') %} {% block style %} diff --git a/canaille/templates/admin/authorization_view.html b/canaille/templates/admin/authorization_view.html index 3424a132..3ad31082 100644 --- a/canaille/templates/admin/authorization_view.html +++ b/canaille/templates/admin/authorization_view.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends theme('base.html') %} {% import 'fomanticui.j2' as sui %} {% block content %} diff --git a/canaille/templates/admin/client_add.html b/canaille/templates/admin/client_add.html index e628f4e2..0c9b1c4f 100644 --- a/canaille/templates/admin/client_add.html +++ b/canaille/templates/admin/client_add.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends theme('base.html') %} {% import 'fomanticui.j2' as sui %} {% block content %} diff --git a/canaille/templates/admin/client_edit.html b/canaille/templates/admin/client_edit.html index 9edd16e3..0a7c602d 100644 --- a/canaille/templates/admin/client_edit.html +++ b/canaille/templates/admin/client_edit.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends theme('base.html') %} {% import 'fomanticui.j2' as sui %} {% block script %} diff --git a/canaille/templates/admin/client_list.html b/canaille/templates/admin/client_list.html index 0152d9a6..23671074 100644 --- a/canaille/templates/admin/client_list.html +++ b/canaille/templates/admin/client_list.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends theme('base.html') %} {% block style %} diff --git a/canaille/templates/admin/token_list.html b/canaille/templates/admin/token_list.html index 7860f9fa..67fd89a2 100644 --- a/canaille/templates/admin/token_list.html +++ b/canaille/templates/admin/token_list.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends theme('base.html') %} {% block style %} diff --git a/canaille/templates/admin/token_view.html b/canaille/templates/admin/token_view.html index 2d8980c1..79038e48 100644 --- a/canaille/templates/admin/token_view.html +++ b/canaille/templates/admin/token_view.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends theme('base.html') %} {% import 'fomanticui.j2' as sui %} {% block content %} diff --git a/canaille/templates/authorize.html b/canaille/templates/authorize.html index 40e3c9a8..2661df74 100644 --- a/canaille/templates/authorize.html +++ b/canaille/templates/authorize.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends theme('base.html') %} {% block content %}