forked from Github-Mirrors/canaille
use flask-themer to allow theme customization
This commit is contained in:
parent
2aac2a0c6a
commit
c0f53c8e6e
39 changed files with 240 additions and 37 deletions
|
@ -11,6 +11,7 @@ It aims to be very light, simple to install and simple to maintain. Its main fea
|
||||||
- OpenID Connect support;
|
- OpenID Connect support;
|
||||||
- No outdated or exotic protocol support;
|
- No outdated or exotic protocol support;
|
||||||
- No additional database required. Everything is stored in your LDAP server;
|
- 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!
|
- The code is easy to read and easy to edit, and you should!
|
||||||
|
|
||||||
# Screenshots
|
# Screenshots
|
||||||
|
|
|
@ -15,8 +15,9 @@ import canaille.account
|
||||||
import canaille.groups
|
import canaille.groups
|
||||||
import canaille.well_known
|
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_babel import Babel
|
||||||
|
from flask_themer import Themer, render_template, FileSystemThemeLoader
|
||||||
|
|
||||||
from .flaskutils import current_user
|
from .flaskutils import current_user
|
||||||
from .ldaputils import LDAPObject
|
from .ldaputils import LDAPObject
|
||||||
|
@ -120,6 +121,18 @@ def setup_app(app):
|
||||||
|
|
||||||
babel = Babel(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
|
@babel.localeselector
|
||||||
def get_locale():
|
def get_locale():
|
||||||
user = getattr(g, "user", None)
|
user = getattr(g, "user", None)
|
||||||
|
@ -137,6 +150,11 @@ def setup_app(app):
|
||||||
if user is not None:
|
if user is not None:
|
||||||
return user.timezone
|
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
|
@app.before_request
|
||||||
def before_request():
|
def before_request():
|
||||||
setup_ldap_connection(app)
|
setup_ldap_connection(app)
|
||||||
|
|
|
@ -7,11 +7,11 @@ from flask import (
|
||||||
url_for,
|
url_for,
|
||||||
current_app,
|
current_app,
|
||||||
abort,
|
abort,
|
||||||
render_template,
|
|
||||||
redirect,
|
redirect,
|
||||||
session,
|
session,
|
||||||
)
|
)
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
from flask_themer import render_template
|
||||||
from werkzeug.datastructures import CombinedMultiDict, FileStorage
|
from werkzeug.datastructures import CombinedMultiDict, FileStorage
|
||||||
from .forms import (
|
from .forms import (
|
||||||
LoginForm,
|
LoginForm,
|
||||||
|
|
|
@ -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.models import AuthorizationCode
|
||||||
from canaille.flaskutils import admin_needed
|
from canaille.flaskutils import admin_needed
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import datetime
|
import datetime
|
||||||
import wtforms
|
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_wtf import FlaskForm
|
||||||
from flask_babel import lazy_gettext as _
|
from flask_babel import lazy_gettext as _
|
||||||
from werkzeug.security import gen_salt
|
from werkzeug.security import gen_salt
|
||||||
|
|
|
@ -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 flask_babel import gettext as _
|
||||||
from canaille.flaskutils import admin_needed
|
from canaille.flaskutils import admin_needed
|
||||||
from canaille.mails import profile_hash
|
from canaille.mails import profile_hash
|
||||||
|
|
|
@ -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.models import Token
|
||||||
from canaille.flaskutils import admin_needed
|
from canaille.flaskutils import admin_needed
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,11 @@ LOGO = "/static/img/canaille-head.png"
|
||||||
# Your favicon. If unset the LOGO will be used.
|
# Your favicon. If unset the LOGO will be used.
|
||||||
FAVICON = "/static/img/canaille-c.png"
|
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
|
# If unset, language is detected
|
||||||
# LANGUAGE = "en"
|
# LANGUAGE = "en"
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,25 @@ from cryptography.hazmat.primitives import serialization as crypto_serialization
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
from cryptography.hazmat.backends import default_backend as crypto_default_backend
|
from cryptography.hazmat.backends import default_backend as crypto_default_backend
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationException(Exception):
|
class ConfigurationException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def validate(config, validate_remote=False):
|
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"]):
|
if not os.path.exists(config["JWT"]["PUBLIC_KEY"]):
|
||||||
raise ConfigurationException(
|
raise ConfigurationException(
|
||||||
f'Public key does not exist {config["JWT"]["PUBLIC_KEY"]}'
|
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"]}'
|
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):
|
def validate_ldap_configuration(config):
|
||||||
from canaille.models import User, Group
|
from canaille.models import User, Group
|
||||||
|
@ -127,6 +134,16 @@ def validate_smtp_configuration(config):
|
||||||
) from exc
|
) 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):
|
def setup_dev_keypair(config):
|
||||||
if os.path.exists(config["JWT"]["PUBLIC_KEY"]) or os.path.exists(
|
if os.path.exists(config["JWT"]["PUBLIC_KEY"]) or os.path.exists(
|
||||||
config["JWT"]["PRIVATE_KEY"]
|
config["JWT"]["PRIVATE_KEY"]
|
||||||
|
|
|
@ -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 flask_babel import gettext
|
||||||
from canaille.models import Consent, Client
|
from canaille.models import Consent, Client
|
||||||
from canaille.flaskutils import user_needed
|
from canaille.flaskutils import user_needed
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from flask import (
|
from flask import (
|
||||||
Blueprint,
|
Blueprint,
|
||||||
render_template,
|
|
||||||
redirect,
|
redirect,
|
||||||
url_for,
|
url_for,
|
||||||
request,
|
request,
|
||||||
|
@ -9,6 +8,7 @@ from flask import (
|
||||||
abort,
|
abort,
|
||||||
)
|
)
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
from flask_themer import render_template
|
||||||
|
|
||||||
from .flaskutils import moderator_needed
|
from .flaskutils import moderator_needed
|
||||||
from .forms import GroupForm
|
from .forms import GroupForm
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import hashlib
|
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_babel import gettext as _
|
||||||
|
from flask_themer import render_template
|
||||||
from .apputils import logo, send_email
|
from .apputils import logo, send_email
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,11 @@ from flask import (
|
||||||
session,
|
session,
|
||||||
redirect,
|
redirect,
|
||||||
abort,
|
abort,
|
||||||
render_template,
|
|
||||||
jsonify,
|
jsonify,
|
||||||
flash,
|
flash,
|
||||||
)
|
)
|
||||||
from flask_babel import gettext, lazy_gettext as _
|
from flask_babel import gettext, lazy_gettext as _
|
||||||
|
from flask_themer import render_template
|
||||||
from .models import User, Client, Consent
|
from .models import User, Client, Consent
|
||||||
from .oauth2utils import (
|
from .oauth2utils import (
|
||||||
authorization,
|
authorization,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
{% import 'fomanticui.j2' as sui %}
|
{% import 'fomanticui.j2' as sui %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
|
|
||||||
{% block style %}
|
{% block style %}
|
||||||
<link href="/static/datatables/jquery.dataTables.min.css" rel="stylesheet">
|
<link href="/static/datatables/jquery.dataTables.min.css" rel="stylesheet">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
{% import 'fomanticui.j2' as sui %}
|
{% import 'fomanticui.j2' as sui %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
{% import 'fomanticui.j2' as sui %}
|
{% import 'fomanticui.j2' as sui %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
{% import 'fomanticui.j2' as sui %}
|
{% import 'fomanticui.j2' as sui %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
|
|
||||||
{% block style %}
|
{% block style %}
|
||||||
<link href="/static/datatables/jquery.dataTables.min.css" rel="stylesheet">
|
<link href="/static/datatables/jquery.dataTables.min.css" rel="stylesheet">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
|
|
||||||
{% block style %}
|
{% block style %}
|
||||||
<link href="/static/datatables/jquery.dataTables.min.css" rel="stylesheet">
|
<link href="/static/datatables/jquery.dataTables.min.css" rel="stylesheet">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
{% import 'fomanticui.j2' as sui %}
|
{% import 'fomanticui.j2' as sui %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="ui segment">
|
<div class="ui segment">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
|
|
||||||
{% block style %}
|
{% block style %}
|
||||||
<link href="/static/datatables/jquery.dataTables.min.css" rel="stylesheet">
|
<link href="/static/datatables/jquery.dataTables.min.css" rel="stylesheet">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="ui error message">
|
<div class="ui error message">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
{% import 'fomanticui.j2' as sui %}
|
{% import 'fomanticui.j2' as sui %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
{% import 'fomanticui.j2' as sui %}
|
{% import 'fomanticui.j2' as sui %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
{% import 'fomanticui.j2' as sui %}
|
{% import 'fomanticui.j2' as sui %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="ui segment">
|
<div class="ui segment">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="ui segment">
|
<div class="ui segment">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
{% import 'fomanticui.j2' as sui %}
|
{% import 'fomanticui.j2' as sui %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
{% import 'fomanticui.j2' as sui %}
|
{% import 'fomanticui.j2' as sui %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
{% import 'fomanticui.j2' as sui %}
|
{% import 'fomanticui.j2' as sui %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
{% import 'fomanticui.j2' as sui %}
|
{% import 'fomanticui.j2' as sui %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'base.html' %}
|
{% extends theme('base.html') %}
|
||||||
|
|
||||||
{% block style %}
|
{% block style %}
|
||||||
<link href="/static/datatables/jquery.dataTables.min.css" rel="stylesheet">
|
<link href="/static/datatables/jquery.dataTables.min.css" rel="stylesheet">
|
||||||
|
|
|
@ -17,6 +17,11 @@ LOGO = "/static/img/canaille-head.png"
|
||||||
# Your favicon. If unset the LOGO will be used.
|
# Your favicon. If unset the LOGO will be used.
|
||||||
FAVICON = "/static/img/canaille-c.png"
|
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
|
# If unset, language is detected
|
||||||
# LANGUAGE = "en"
|
# LANGUAGE = "en"
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ install_requires =
|
||||||
email_validator
|
email_validator
|
||||||
flask
|
flask
|
||||||
flask-babel
|
flask-babel
|
||||||
|
flask-themer
|
||||||
flask-wtf
|
flask-wtf
|
||||||
python-ldap
|
python-ldap
|
||||||
sentry-sdk[flask]
|
sentry-sdk[flask]
|
||||||
|
|
107
tests/fixtures/themes/test/base.html
vendored
Normal file
107
tests/fixtures/themes/test/base.html
vendored
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
{% import 'flask.j2' as flask %}
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
|
||||||
|
<title>{% block title %}{{ website_name|default("Canaille") }} {% trans %}authorization interface{% endtrans %}{% endblock %}</title>
|
||||||
|
|
||||||
|
<link href="/static/fomanticui/semantic.min.css" rel="stylesheet">
|
||||||
|
<link href="/static/fonts/lato.css" rel="stylesheet">
|
||||||
|
<link href="/static/css/base.css" rel="stylesheet">
|
||||||
|
{% if logo_url %}<link rel="icon" href="{{ favicon_url }}">{% endif %}
|
||||||
|
{% block style %}{% endblock %}
|
||||||
|
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script>window.html5 || document.write('<script src="/static/html5shiv/html5shiv.min.js"><\/script>')</script>
|
||||||
|
<script src="/static/js/respond.min.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{% block menu %}
|
||||||
|
{% if user and menu %}
|
||||||
|
<nav class="ui stackable labeled icon menu container">
|
||||||
|
{% if logo_url %}
|
||||||
|
<div class="header item">
|
||||||
|
<a href="/" class="logo">
|
||||||
|
<img class="ui image" src="{{ logo_url }}" alt="{{ website_name }}" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<a class="item {% if menuitem == "profile" %}active{% endif %}"
|
||||||
|
href="{{ url_for('account.profile_edition', username=user.uid[0]) }}">
|
||||||
|
<i class="id card icon"></i>
|
||||||
|
{% trans %}My profile{% endtrans %}
|
||||||
|
</a>
|
||||||
|
<a class="item {% if menuitem == "consents" %}active{% endif %}"
|
||||||
|
href="{{ url_for('consents.consents') }}">
|
||||||
|
<i class="handshake icon"></i>
|
||||||
|
{% trans %}My consents{% endtrans %}
|
||||||
|
</a>
|
||||||
|
{% if user.moderator %}
|
||||||
|
<a class="item {% if menuitem == "users" %}active{% endif %}"
|
||||||
|
href="{{ url_for('account.users') }}">
|
||||||
|
<i class="users icon"></i>
|
||||||
|
{% trans %}Users{% endtrans %}
|
||||||
|
</a>
|
||||||
|
<a class="item {% if menuitem == "groups" %}active{% endif %}"
|
||||||
|
href="{{ url_for('groups.groups') }}">
|
||||||
|
<i class="users cog icon"></i>
|
||||||
|
{% trans %}Groups{% endtrans %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if user.admin %}
|
||||||
|
<div class="ui dropdown item {% if menuitem == "admin" %}active{% endif %}">
|
||||||
|
<i class="settings icon"></i>
|
||||||
|
Admin
|
||||||
|
<div class="menu">
|
||||||
|
<a class="item" href="{{ url_for('admin_clients.index') }}">
|
||||||
|
<i class="plug icon"></i>
|
||||||
|
{% trans %}Clients{% endtrans %}
|
||||||
|
</a>
|
||||||
|
<a class="item" href="{{ url_for('admin_tokens.index') }}">
|
||||||
|
<i class="key icon"></i>
|
||||||
|
{% trans %}Tokens{% endtrans %}
|
||||||
|
</a>
|
||||||
|
<a class="item" href="{{ url_for('admin_authorizations.index') }}">
|
||||||
|
<i class="user secret icon"></i>
|
||||||
|
{% trans %}Codes{% endtrans %}
|
||||||
|
</a>
|
||||||
|
<a class="item" href="">
|
||||||
|
<i class="handshake icon"></i>
|
||||||
|
{% trans %}Consents{% endtrans %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<a class="item" href="{{ url_for('account.logout') }}">
|
||||||
|
<i class="sign out alternate icon"></i>
|
||||||
|
{% trans %}Log out{% endtrans %}
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<div class="ui container">
|
||||||
|
<div class="content">
|
||||||
|
{{ flask.messages() }}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<a href="{{ url_for('account.about') }}">{{ _("About canaille") }}</a>
|
||||||
|
</footer>
|
||||||
|
<script src="/static/jquery/jquery.min.js"></script>
|
||||||
|
<script src="/static/fomanticui/semantic.min.js"></script>
|
||||||
|
<script src="/static/js/base.js"></script>
|
||||||
|
{% block script %}{% endblock %}
|
||||||
|
|
||||||
|
TEST_THEME
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,8 +1,11 @@
|
||||||
import ldap
|
import ldap
|
||||||
import mock
|
import mock
|
||||||
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
|
from canaille import create_app
|
||||||
from canaille.commands import cli
|
from canaille.commands import cli
|
||||||
from canaille.configuration import validate, ConfigurationException
|
from canaille.configuration import validate, ConfigurationException
|
||||||
|
from flask_webtest import TestApp
|
||||||
|
|
||||||
|
|
||||||
def test_ldap_connection_no_remote(configuration):
|
def test_ldap_connection_no_remote(configuration):
|
||||||
|
@ -104,3 +107,43 @@ def test_check_command_fail(testclient):
|
||||||
testclient.app.config["LDAP"]["URI"] = "ldap://invalid-ldap.com"
|
testclient.app.config["LDAP"]["URI"] = "ldap://invalid-ldap.com"
|
||||||
runner = testclient.app.test_cli_runner()
|
runner = testclient.app.test_cli_runner()
|
||||||
runner.invoke(cli, ["check"])
|
runner.invoke(cli, ["check"])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def themed_testclient(app, configuration):
|
||||||
|
configuration["TESTING"] = True
|
||||||
|
|
||||||
|
root = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
test_theme_path = os.path.join(root, "fixtures", "themes", "test")
|
||||||
|
configuration["THEME"] = test_theme_path
|
||||||
|
|
||||||
|
configuration["AUTHLIB_INSECURE_TRANSPORT"] = "true"
|
||||||
|
app = create_app(configuration)
|
||||||
|
|
||||||
|
return TestApp(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_theme(testclient, themed_testclient):
|
||||||
|
res = testclient.get("/login")
|
||||||
|
assert "TEST_THEME" not in res
|
||||||
|
|
||||||
|
res = themed_testclient.get("/login")
|
||||||
|
assert "TEST_THEME" in res
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_theme(configuration):
|
||||||
|
validate(configuration, validate_remote=False)
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
ConfigurationException,
|
||||||
|
match=r"Cannot find theme",
|
||||||
|
):
|
||||||
|
configuration["THEME"] = "invalid"
|
||||||
|
validate(configuration, validate_remote=False)
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
ConfigurationException,
|
||||||
|
match=r"Cannot find theme",
|
||||||
|
):
|
||||||
|
configuration["THEME"] = "/path/to/invalid"
|
||||||
|
validate(configuration, validate_remote=False)
|
||||||
|
|
Loading…
Reference in a new issue