build(init): initialisation depuis le cookiecutter wagtail

pull/1/head
François Poulain 2020-02-21 11:44:58 +01:00 commité par François Poulain
révision e790585c3a
86 fichiers modifiés avec 5587 ajouts et 0 suppressions

3
.babelrc Normal file
Voir le fichier

@ -0,0 +1,3 @@
{
"presets": [ "@babel/preset-env" ]
}

14
.browserslistrc Normal file
Voir le fichier

@ -0,0 +1,14 @@
# Browsers that we support
# see: https://github.com/browserslist/browserslist#readme
>= 1%
last 1 major version
not dead
Chrome >= 45
Firefox >= 38
Edge >= 12
Explorer >= 10
iOS >= 9
Safari >= 9
Android >= 4.4
Opera >= 30

29
.editorconfig Normal file
Voir le fichier

@ -0,0 +1,29 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{py,rst,ini}]
indent_style = space
indent_size = 4
[*.py]
line_length = 80
known_first_party = gvot
multi_line_output = 3
default_section = THIRDPARTY
[*.{html,css,scss,js,json,yml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

32
.eslintrc.json Normal file
Voir le fichier

@ -0,0 +1,32 @@
{
"root": true,
"parser": "babel-eslint",
"extends": [
"xo/esnext",
"xo/browser"
],
"rules": {
"capitalized-comments": "off",
"indent": [
"error",
2,
{
"SwitchCase": 1
}
],
"max-params": [
"warn",
5
],
"multiline-ternary": [
"error",
"always-multiline"
],
"new-cap": "off",
"object-curly-spacing": [
"error",
"always"
],
"prefer-destructuring": "off"
}
}

3
.gitattributes externe Normal file
Voir le fichier

@ -0,0 +1,3 @@
gvot/static/** -diff
assets/img/** -diff
assets/fonts/** -diff

42
.gitignore externe Normal file
Voir le fichier

@ -0,0 +1,42 @@
# Editors
*~
*.sw[po]
# Python
*.py[cod]
__pycache__
# Virtual environment
.env
venv
# Logs
logs
*.log
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
htmlcov
# Translations
*.mo
*.pot
# NPM
node_modules/
# Databases
sqlite.db
# Local configuration
config.env
# Local overrides and variable content
local/
var/
# Asset builds hidden in developp branch
gvot/static/

12
.stylelintrc Normal file
Voir le fichier

@ -0,0 +1,12 @@
{
"extends": [
"stylelint-config-twbs-bootstrap/scss"
],
"rules": {
"value-list-comma-newline-after": "always-multi-line",
"value-list-comma-newline-before": "never-multi-line",
"value-list-comma-space-after": "always-single-line",
"scss/at-function-named-arguments": null,
"scss/dollar-variable-default": null
}
}

8
CHANGELOG.md Normal file
Voir le fichier

@ -0,0 +1,8 @@
# Changelog
All notable changes to GvoT will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
### Added

1
CONTRIBUTORS.txt Normal file
Voir le fichier

@ -0,0 +1 @@
This software is developped by Cliss XXI.

661
LICENSE Normal file

Fichier diff supprimé car celui-ci est trop grand Voir la Diff

162
Makefile Normal file
Voir le fichier

@ -0,0 +1,162 @@
# -*- mode: makefile-gmake -*-
## Définition des variables
# Le nom de l'exécutable Python à utiliser ou son chemin absolu
# (ex. : python ou python3).
PYTHON_EXE := python3
# S'il faut utiliser un environnement virtuel (y ou n).
USE_VENV := y
# Configuration de l'environnement virtuel.
VENV_DIR := venv
VENV_OPT := --system-site-packages
# Définis les chemins et options des exécutables.
PYTHON_EXE_BASENAME := $(shell basename $(PYTHON_EXE))
VENV_PYTHON := --python=$(PYTHON_EXE_BASENAME)
ifeq ($(USE_VENV), y)
PYTHON := $(VENV_DIR)/bin/$(PYTHON_EXE_BASENAME)
PIP := $(VENV_DIR)/bin/pip
else
PYTHON := $(shell which $(PYTHON_EXE))
PIP := $(shell which pip)
endif
# Détermine si black est présent.
USE_BLACK := $(shell $(PYTHON) -c 'import black; print("1")' 2>/dev/null)
# Détermine s'il faut charger le fichier de configuration.
ifneq ($(READ_CONFIG_FILE), 0)
READ_CONFIG_FILE := 1
else
READ_CONFIG_FILE := 0
endif
# Détermine l'environnement à utiliser.
DEFAULT_ENV := production
ifndef ENV
ifeq ($(READ_CONFIG_FILE), 1)
# Commence par chercher la dernière valeur de DJANGO_SETTINGS_MODULE,
# puis de ENV s'il n'y en a pas, ou utilise l'environnement par défaut.
ENV = $(shell \
sed -n -e '/^DJANGO_SETTINGS_MODULE/s/[^.]*\.settings\.\([^.]*\)/\1/p' \
-e '/^ENV/s/[^=]*=\(.*\)/\1/p' config.env 2> /dev/null \
| tail -n 1 | grep -Ee '^..*' || echo "$(DEFAULT_ENV)")
else
ifdef DJANGO_SETTINGS_MODULE
ENV = $(shell echo $(DJANGO_SETTINGS_MODULE) | cut -d. -f3)
else
ENV := $(DEFAULT_ENV)
endif # ifdef DJANGO_SETTINGS_MODULE
endif # ifeq READ_CONFIG_FILE
endif # ifndef ENV
# Définis EDITOR pour l'édition interactive.
ifndef EDITOR
ifdef VISUAL
EDITOR := $(VISUAL)
else
EDITOR := vi
endif
endif
# Définition des cibles -------------------------------------------------------
.PHONY: clean-pyc clean-build clean-static clear-venv help check check-config
.DEFAULT_GOAL := help
# Commentaire d'une cible : #-> interne ##-> aide production+dev ###-> aide dev
help: ## affiche cette aide
ifeq ($(ENV), production)
@perl -nle'print $& if m{^[a-zA-Z_-]+:[^#]*?## .*$$}' $(MAKEFILE_LIST) \
| sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'
else
@perl -nle'print $& if m{^[a-zA-Z_-]+:[^#]*?###? .*$$}' $(MAKEFILE_LIST) \
| sort | awk 'BEGIN {FS = ":.*?###? "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'
endif
clean: clean-build clean-pyc clean-static ## nettoie tous les fichiers temporaires
clean-build: ### nettoie les fichiers de construction du paquet
rm -rf build/
rm -rf dist/
rm -rf *.egg-info
clean-pyc: ### nettoie les fichiers temporaires python
find gvot/ \
\( -name '*.pyc' -o -name '*.pyo' -o -name '*~' \) -exec rm -f {} +
clean-static: ### nettoie les fichiers "static" collectés
rm -rf var/static
init: create-venv config.env update ## initialise l'environnement et l'application
config.env:
ifeq ($(READ_CONFIG_FILE), 1)
cp config.env.example config.env
chmod go-rwx config.env
$(EDITOR) config.env
endif
update: check-config install-deps migrate static ## mets à jour l'application et ses dépendances
touch gvot/wsgi.py
check: check-config ## vérifie la configuration de l'instance
$(PYTHON) manage.py check
check-config:
@find . -maxdepth 1 -name config.env -perm /o+rwx -exec false {} + || \
{ echo "\033[31mErreur :\033[0m les permissions de config.env ne sont pas bonnes, \
vous devriez au moins faire : chmod o-rwx config.env"; false; }
install-deps: ## installe les dépendances de l'application
$(PIP) install --upgrade --requirement requirements/$(ENV).txt
migrate: ## mets à jour le schéma de la base de données
$(PYTHON) manage.py migrate
static: ## collecte les fichiers statiques
ifeq ($(ENV), production)
@echo "Collecte des fichiers statiques..."
$(PYTHON) manage.py collectstatic --no-input --verbosity 0
endif
## Cibles liées à l'environnement virtuel
create-venv: $(PYTHON)
$(PYTHON):
ifeq ($(USE_VENV), y)
virtualenv $(VENV_OPT) $(VENV_PYTHON) $(VENV_DIR)
else
@echo "\033[31mErreur !\033[0m Impossible de trouver l'exécutable Python $(PYTHON)"
@exit 1
endif
clear-venv: ## supprime l'environnement virtuel
-rm -rf $(VENV_DIR)
## Cibles pour le développement
serve: ### démarre un serveur local pour l'application
$(PYTHON) manage.py runserver
test: ### lance les tests de l'application
$(PYTHON) -m pytest --cov --cov-report=term:skip-covered
cov: test ### vérifie la couverture de code
$(PYTHON) -m coverage html
@echo open htmlcov/index.html
lint: ### vérifie la syntaxe et le code python
@$(PYTHON) -m flake8 gvot \
|| echo "\033[31m[flake8]\033[0m Veuillez corriger les erreurs ci-dessus."
@$(PYTHON) -m isort --check --recursive gvot \
|| echo "\033[31m[isort]\033[0m Veuillez corriger l'ordre des imports avec : make fix-lint"
ifdef USE_BLACK
@$(PYTHON) -m black --check gvot
endif
fix-lint: ### corrige la syntaxe et ordonne les imports python
$(PYTHON) -m isort --recursive gvot
ifdef USE_BLACK
$(PYTHON) -m black gvot
endif

258
README.md Normal file
Voir le fichier

@ -0,0 +1,258 @@
# GvoT
Logiciel de votation
**Table of content**
- [Give a try](#give-a-try)
- [Installation](#installation)
- [Deployment](#deployment)
- [Structure](#structure)
- [Development](#development)
## Give a try
On a Debian-based host - running at least Debian Stretch:
```
$ sudo apt install python3 virtualenv git make
$ git clone https://forge.cliss21.org/cliss21/gvot
$ cd gvot/
$ make init
A configuration will be created interactively; uncomment
ENV=development
$ make test # optional
$ make serve
```
Then visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/) in your web browser.
## Installation
### Requirements
On a Debian-based host - running at least Debian Stretch, you will need the
following packages:
- python3
- virtualenv
- make
- git (recommended for getting the source)
- python3-mysqldb (optional, in case of a MySQL / MariaDB database)
- python3-psycopg2 (optional, in case of a PostgreSQL database)
### Quick start
It assumes that you already have the application source code locally - the best
way is by cloning this repository - and that you are in this folder.
1. Define your local configuration in a file named `config.env`, which can be
copied from `config.env.example` and edited to suits your needs.
Depending on your environment, you will have to create your database and the
user at first.
2. Run `make init`.
Note that if there is no `config.env` file, it will be created interactively.
That's it! Your environment is now initialized with the application installed.
To update it, once the source code is checked out, simply run `make update`.
You can also check that your application is well configured by running
`make check`.
### Manual installation
If you don't want to use the `Makefile` facilities, here is what is done behind the scene.
It assumes that you have downloaded the last release of GvoT,
extracted it and that you moved to that folder.
1. Start by creating a new virtual environment under `./venv` and activate it:
$ virtualenv --system-site-packages ./venv
$ source ./venv/bin/activate
2. Install the required Python packages depending on your environment:
$ pip install -r requirements/production.txt
... or ...
$ pip install -r requirements/development.txt
3. Configure the application by setting the proper environment variables
depending on your environment. You can use the `config.env.example` which
give you the main variables with example values.
$ cp config.env.example config.env
$ nano config.env
$ chmod go-rwx config.env
Note that this `./config.env` file will be loaded by default when the
application starts. If you don't want that, just move this file away or set
the `READ_CONFIG_FILE` environment variable to `0`.
4. Create the database tables - it assumes that you have created the database
and set the proper configuration to use it:
$ ./manage.py migrate
That's it! You should now be able to start the Django development server to
check that everything is working fine with:
$ ./manage.py runserver
## Deployment
Here is an example deployment using NGINX - as the Web server - and uWSGI - as
the application server.
The uWSGI configuration doesn't require a special configuration, except that we
are using Python 3 and a virtual environment. Note that if you serve the
application on a sub-location, you will have to add `route-run = fixpathinfo:`
to your uWSGI configuration (from
[v2.0.11](https://uwsgi-docs.readthedocs.io/en/latest/Changelog-2.0.11.html#fixpathinfo-routing-action)).
In the `server` block of your NGINX configuration, add the following blocks and
set the path to your application instance and to the uWSGI socket:
```
location / {
include uwsgi_params;
uwsgi_pass unix:<uwsgi_socket_path>;
}
location /media {
alias <app_instance_path>/var/media;
}
location /static {
alias <app_instance_path>/var/static;
# Optional: don't log access to assets
access_log off;
}
location = /favicon.ico {
alias <app_instance_path>/var/static/favicon/favicon.ico;
# Optional: don't log access to the favicon
access_log off;
}
```
## Structure
### Overview
All the application files - e.g. Django code including settings, templates and
statics - are located into `gvot/`.
Two environments are defined - either for requirements and settings:
- `development`: for local application development and testing. It uses a
SQLite3 database and enable debugging by default, add some useful settings
and applications for development purpose - i.e. the `django-debug-toolbar`.
- `production`: for production. It checks that configuration is set and
correct, try to optimize performances and enforce some settings - i.e. HTTPS
related ones.
### Local changes
You can override and extend statics and templates locally. This can be useful
if you have to change the logo for a specific instance for example. For that,
just put your files under the `local/static/` and `local/templates/` folders.
Regarding the statics, do not forget to collect them after that. Note also that
the `local/` folder is ignored by *git*.
### Variable content
All the variable content - e.g. user-uploaded media, collected statics - are
stored inside the `var/` folder. It is also ignored by *git* as it's specific
to each application installation.
So, you will have to configure your Web server to serve the `var/media/` and
`var/static/` folders, which should point to `/media/` and `/static/`,
respectively.
## Development
The easiest way to deploy a development environment is by using the `Makefile`.
Before running `make init`, ensure that you have either set `ENV=development`
in the `config.env` file or have this environment variable. Note that you can
still change this variable later and run `make init` again.
There is some additional rules when developing, which are mainly wrappers for
`manage.py`. You can list all of them by running `make help`. Here are the main ones:
- `make serve`: run a development server
- `make test`: test the whole application
- `make lint`: check the Python code syntax
### Assets
The assets - e.g. CSS, JavaScript, images, fonts - are generated using a
[Gulp](https://gulpjs.com/)-powered build system with these features:
- SCSS compilation and prefixing
- JavaScript module bundling with webpack
- Styleguide and components preview
- Built-in BrowserSync server
- Compression for production builds
The source files live in `assets/`, and the styleguide in `styleguide/`.
#### Requirements
You will need to have [npm](https://www.npmjs.com/) installed on your system.
If you are running Debian, do not rely on the npm package which is either
outdated or removed - starting from Debian Stretch. Instead, here is a way
to install the last version as a regular user:
1. Ensure that you have the following Debian packages installed, from at least
`stretch-backports`:
- nodejs
- node-rimraf
2. Set the npm's installation prefix as an environment variable:
$ export npm_config_prefix=~/.node_modules
3. Retrieve and execute the last npm's installation script:
$ curl -L https://www.npmjs.com/install.sh | sh
4. Add the npm's binary folder to your environment variables:
$ export PATH="${HOME}/.node_modules/bin:${PATH}"
In order to keep those environment variables the next time you will log in,
you can append the following lines to the end of your `~/.profile` file:
```bash
if [ -d "${HOME}/.node_modules/bin" ] ; then
PATH="${HOME}/.node_modules/bin:${PATH}"
export npm_config_prefix=~/.node_modules
fi
```
5. That's it! You can check that npm is now installed by running the following:
$ npm --version
#### Usage
Start by installing the application dependencies - which are defined in
`package.json` - by running: `npm install`.
The following tasks are then available:
- `npm run build`: build all the assets for development and production use,
and put them in the static folder - e.g `gvot/static`.
- `npm run styleguide`: run a server with the styleguide and watch for file
changes.
- `npm run serve`: run a proxy server to the app - which must already be served on
`localhost:8000` - with the styleguide on `/styleguide` and watch for file
changes.
- `npm run lint`: lint the JavaScript and the SCSS code.
In production, only the static files will be used. It is recommended to commit
the compiled assets just before a new release only. This will prevent to have a
growing repository due to the minified files.
## License
GvoT is developed by Cliss XXI and licensed under the
[AGPLv3+](LICENSE).

18
assets/js/app.js Normal file
Voir le fichier

@ -0,0 +1,18 @@
import $ from 'jquery';
import './vendor/bootstrap';
// Export jQuery for external usage
window.jQuery = window.$ = $; // eslint-disable-line no-multi-assign
// -----------------------------------------------------------------------------
// Main application
// -----------------------------------------------------------------------------
$(() => {
$('.no-js').removeClass('no-js');
// Initialize Popover and Tooltip on the whole page
$('[data-toggle="popover"]').popover();
$('[data-toggle="tooltip"]').tooltip();
});

23
assets/js/vendor/bootstrap.js externe Normal file
Voir le fichier

@ -0,0 +1,23 @@
// -----------------------------------------------------------------------------
// Bootstrap 4
// -----------------------------------------------------------------------------
// see: ../../node_modules/bootstrap/js/src/index.js
// Import all Bootstrap components
import 'bootstrap';
// ... or import them individually
// import 'bootstrap/js/dist/util';
// import 'bootstrap/js/dist/alert';
// import 'bootstrap/js/dist/button';
// import 'bootstrap/js/dist/carousel';
// import 'bootstrap/js/dist/collapse';
// import 'bootstrap/js/dist/dropdown';
// import 'bootstrap/js/dist/modal';
// import 'bootstrap/js/dist/popover';
// import 'bootstrap/js/dist/scrollspy';
// import 'bootstrap/js/dist/tab';
// import 'bootstrap/js/dist/toast';
// import 'bootstrap/js/dist/tooltip';

Voir le fichier

@ -0,0 +1,24 @@
// -----------------------------------------------------------------------------
// Bootstrap's configuration for the application
// -----------------------------------------------------------------------------
// see: ../../node_modules/bootstrap/scss/_variables.scss
// Color system
// You can generate a color scheme with:
// https://palx.jxnblk.com/
$blue: #007bff;
$indigo: #6610f2;
$purple: #6f42c1;
$pink: #e83e8c;
$red: #dc3545;
$orange: #fd7e14;
$yellow: #ffc107;
$green: #28a745;
$teal: #20c997;
$cyan: #17a2b8;
// Add 'error' as an alternative to 'danger' since it is used by Django.
$theme-colors: (
"error": $red
);

Voir le fichier

@ -0,0 +1,8 @@
// -----------------------------------------------------------------------------
// Application-wide variables
// -----------------------------------------------------------------------------
/// Path to fonts and images folders, relative to css/app.css.
/// @type String
$font-path: "../fonts";
$img-path: "../img";

21
assets/scss/app.scss Normal file
Voir le fichier

@ -0,0 +1,21 @@
@charset "utf-8";
// Configuration and helpers
@import "abstracts/variables";
@import "abstracts/variables-bootstrap";
// Vendors
@import "vendor/bootstrap";
// Base styles
@import "base/fonts";
// Layout-related sections
//@import "layout/header";
//@import "layout/footer";
// Components
@import "components/forms";
// Page-specific styles
//@import "pages/home";

Voir le fichier

@ -0,0 +1,3 @@
// -----------------------------------------------------------------------------
// Font faces declarations
// -----------------------------------------------------------------------------

Voir le fichier

@ -0,0 +1,9 @@
// -----------------------------------------------------------------------------
// Forms component's extension
// -----------------------------------------------------------------------------
// Indicate that a form field is required.
.required {
font-size: 90%;
color: $danger;
}

Voir le fichier

@ -0,0 +1,12 @@
@charset "utf-8";
// Configuration and helpers
@import "abstracts/variables";
// Fork Awesome
// ------------
// @link https://forkawesome.github.io/
$fa-font-path: "#{$font-path}/fork-awesome";
@import "fork-awesome/scss/fork-awesome";

48
assets/scss/vendor/_bootstrap.scss externe Normal file
Voir le fichier

@ -0,0 +1,48 @@
// ----------------------------------------------------------------------------
// Bootstrap 4
// ----------------------------------------------------------------------------
// see: ../../node_modules/bootstrap/scss/bootstrap.scss
/// Import all Bootstrap components
@import "bootstrap/scss/bootstrap";
/// ... or import them individually
//@import "bootstrap/scss/functions";
//@import "bootstrap/scss/variables";
//@import "bootstrap/scss/mixins";
//@import "bootstrap/scss/root";
//@import "bootstrap/scss/reboot";
//@import "bootstrap/scss/type";
//@import "bootstrap/scss/images";
//@import "bootstrap/scss/code";
//@import "bootstrap/scss/grid";
//@import "bootstrap/scss/tables";
//@import "bootstrap/scss/forms";
//@import "bootstrap/scss/buttons";
//@import "bootstrap/scss/transitions";
//@import "bootstrap/scss/dropdown";
//@import "bootstrap/scss/button-group";
//@import "bootstrap/scss/input-group";
//@import "bootstrap/scss/custom-forms";
//@import "bootstrap/scss/nav";
//@import "bootstrap/scss/navbar";
//@import "bootstrap/scss/card";
//@import "bootstrap/scss/breadcrumb";
//@import "bootstrap/scss/pagination";
//@import "bootstrap/scss/badge";
//@import "bootstrap/scss/jumbotron";
//@import "bootstrap/scss/alert";
//@import "bootstrap/scss/progress";
//@import "bootstrap/scss/media";
//@import "bootstrap/scss/list-group";
//@import "bootstrap/scss/close";
//@import "bootstrap/scss/toasts";
//@import "bootstrap/scss/modal";
//@import "bootstrap/scss/tooltip";
//@import "bootstrap/scss/popover";
//@import "bootstrap/scss/carousel";
//@import "bootstrap/scss/spinners";
//@import "bootstrap/scss/utilities";
//@import "bootstrap/scss/print";

105
config.env.example Normal file
Voir le fichier

@ -0,0 +1,105 @@
###########################################################
# #
# Edit the following configuration to suits your needs. #
# #
###########################################################
###############################################################################
# MAINS SETTINGS
###############################################################################
# Environment to use within the application.
#
# The environment is used to load the proper settings for your application
# instance. There is two ways for defining it, with the following precedence:
# - DJANGO_SETTINGS_MODULE: the Python path to the settings module to use. It
# allows you to define and use your own settings module. Use it with care!
# Note: the module name will be used as the environment.
# - ENV: the environment to use, which is one of 'production' or 'development'.
#
# Default is the 'production' environment.
#ENV=production
#ENV=development
# The secret key used to provide cryptographic signing.
#
# It should be set to a unique, unpredictable value. On a GNU/Linux system, you
# could generate a new one with:
#
# $ head -c50 /dev/urandom | base64
#
# /!\ Required in production.
#DJANGO_SECRET_KEY=CHANGEME!!!
# A coma-separated string representing the host/domain names that this
# application instance can serve.
#
# /!\ Required in production.
#DJANGO_ALLOWED_HOSTS=example.org,
###############################################################################
# DATABASE SETTINGS
###############################################################################
# Database configuration, as an URI.
#
# In production, the recommended database backend for better performances is
# PostgreSQL - or MySQL if you prefer.
#
# Default is a SQLite database in development only.
#
# /!\ Required in production.
#DJANGO_DATABASE_URL=postgres://user:password@127.0.0.1:5432/gvot
#DJANGO_DATABASE_URL=mysql://user:password@127.0.0.1:3306/gvot
###############################################################################
# EMAILS SETTINGS
###############################################################################
# Email configuration for sending messages, as an URI.
#
# In production, you should either use a local SMTP server or a relay one. The
# URI will be in that case of the form:
#
# PROTOCOL://[USER:PASSWORD@]HOST[:PORT]
#
# PROTOCOL can be smtp, smtp+ssl or smtp+tls. Note that special characters
# in USER and PASSWORD - e.g. @ - must be escaped. It can be achieve with:
#
# $ python3 -c 'from urllib.parse import quote as q;print(q("USER")+":"+q("PASSWORD"))'
#
# Default is the local SMTP server in production and the console in development.
#DJANGO_EMAIL_URL=smtp://localhost:25
# Default email address to use for various automated correspondence.
#
# /!\ Required in production.
#DEFAULT_FROM_EMAIL=webmaster@example.org
# A comma separated list of all the people who get production error
# notifications, following rfc2822 format
#ADMINS='Cliss XXI <francois.poulain@cliss21.org>'
###############################################################################
# MISC SETTINGS
###############################################################################
# URL prefix on which the application is served.
#
# This is used to generate the static and media URLs, but also links to the
# application which require an absolute URL.
#
# Default is '/', e.g. at the domain root.
#APP_LOCATION=/
# Base directory of the app instance, where the local and var folders are
# located.
#
# Default is the current directory.
#BASE_DIR=
# Turn on/off debug mode.
#
# Note that it's always disabled in production.
#DJANGO_DEBUG=off
#DJANGO_DEBUG_TOOLBAR=on

273
gulpfile.js Normal file
Voir le fichier

@ -0,0 +1,273 @@
const gulp = require('gulp');
const plugins = require('gulp-load-plugins');
const merge = require('merge-stream');
const sherpa = require('style-sherpa');
const named = require('vinyl-named');
const webpack = require('webpack');
const webpackStream = require('webpack-stream');
const browser = require('browser-sync').create();
// Load all Gulp plugins into one variable
const $ = plugins({
rename: {
'gulp-touch-fd': 'touch'
}
});
/// Configuration -------------------------------------------------------------
const CONFIG = {
// Proxy target of the BrowserSync'server
SERVER_PROXY: 'http://127.0.0.1:8000',
// Port on which the BrowserSync'server will listen
SERVER_PORT: 8090,
// Paths to other assets which will be copied
ASSETS_FILES: [
{
src: [
'assets/**/*',
'!assets/{img,js,scss}',
'!assets/{img,js,scss}/**/*'
],
dest: ''
},
{
// ForkAwesome
src: 'node_modules/fork-awesome/fonts/*',
dest: 'fonts/fork-awesome'
}
],
// Paths to images which will be compressed and copied
IMAGES_FILES: [
'assets/img/**/*'
],
// Paths to JavaScript entries which will be bundled
JS_ENTRIES: [
'assets/js/app.js'
],
// Paths to Sass files which will be compiled
SASS_ENTRIES: [
'assets/scss/app.scss',
'assets/scss/fork-awesome.scss'
],
// Paths to Sass libraries, which can then be loaded with @import
SASS_INCLUDE_PATHS: [
'node_modules'
],
// Path to the build output, which will never be cleaned
BUILD_PATH: 'gvot/static'
};
/// CSS -----------------------------------------------------------------------
// Compile Sass into CSS.
gulp.task('sass', function() {
return gulp.src(CONFIG.SASS_ENTRIES)
.pipe($.sourcemaps.init())
.pipe($.sass({
includePaths: CONFIG.SASS_INCLUDE_PATHS
}).on('error', $.sass.logError))
.pipe($.autoprefixer())
.pipe($.sourcemaps.write('.'))
.pipe(gulp.dest(`${CONFIG.BUILD_PATH}/css`))
.pipe($.touch())
.pipe(browser.reload({ stream: true }));
});
// Lint Sass files.
gulp.task('lint:sass', function() {
return gulp.src('assets/scss/**/*.scss')
.pipe($.stylelint({
failAfterError: true,
reporters: [
{ formatter: 'verbose', console: true }
]
}));
});
// Compress CSS files.
gulp.task('compress:css', function() {
return gulp.src([
`${CONFIG.BUILD_PATH}/css/*.css`,
`!${CONFIG.BUILD_PATH}/css/*.min.css`
])
.pipe($.cleanCss())
.pipe($.rename({ suffix: '.min' }))
.pipe(gulp.dest(`${CONFIG.BUILD_PATH}/css`));
});
gulp.task('css',
gulp.series('sass', 'compress:css'));
/// JavaScript ----------------------------------------------------------------
let webpackConfig = {
devtool: 'source-map',
mode: 'development',
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
compact: false
}
}
}
]
},
stats: {
chunks: false,
entrypoints: false,
}
}
// Bundle JavaScript module.
gulp.task('javascript', function() {
return gulp.src(CONFIG.JS_ENTRIES)
.pipe(named())
.pipe(webpackStream(webpackConfig, webpack))
.pipe(gulp.dest(`${CONFIG.BUILD_PATH}/js`));
});
// Lint JavaScript source files.
gulp.task('lint:javascript', function() {
return gulp.src('assets/js/**/*.js')
.pipe($.eslint())
.pipe($.eslint.format())
.pipe($.eslint.failAfterError());
});
// Compress JavaScript files.
gulp.task('compress:javascript', function() {
return gulp.src([
`${CONFIG.BUILD_PATH}/js/*.js`,
`!${CONFIG.BUILD_PATH}/js/*.min.js`
])
.pipe($.terser().on('error', e => { console.log(e); }))
.pipe($.rename({ suffix: '.min' }))
.pipe(gulp.dest(`${CONFIG.BUILD_PATH}/js`));
});
gulp.task('scripts',
gulp.series('javascript', 'compress:javascript'));
/// Other assets --------------------------------------------------------------
// Compress and copy images.
gulp.task('images', function() {
return gulp.src(CONFIG.IMAGES_FILES)
.pipe($.imagemin({ progressive: true }))
.pipe(gulp.dest(`${CONFIG.BUILD_PATH}/img`));
});
// Copy other assets files.
gulp.task('copy', function() {
return merge(CONFIG.ASSETS_FILES.map(
item => gulp.src(item.src)
.pipe(gulp.dest(`${CONFIG.BUILD_PATH}/${item.dest}`))
));
});
/// HTML files ----------------------------------------------------------------
// Generate a style guide from the Markdown content.
gulp.task('styleguide', done => {
sherpa('styleguide/index.md', {
output: 'styleguide/index.html',
template: 'styleguide/template.html'
}, done);
});
/// General tasks -------------------------------------------------------------
// Build and compress CSS, JavaScript and other assets.
gulp.task('build',
gulp.parallel('css', 'scripts', 'images', 'copy', 'styleguide'));
// Watch for changes to static assets, Sass and JavaScript.
gulp.task('watch', function() {
gulp.watch([].concat.apply([], CONFIG.ASSETS_FILES.map(a => a.src)),
gulp.series('copy', reload));
gulp.watch('assets/scss/**/*.scss',
gulp.series('sass'));
gulp.watch('assets/js/**/*.js',
gulp.series('javascript', reload));
gulp.watch('assets/img/**/*',
gulp.series('images', reload));
gulp.watch(['styleguide/*', '!styleguide/index.html'],
gulp.series('styleguide', reload));
});
// Run a development server and watch for file changes.
gulp.task('serve',
gulp.series(proxyServer, 'watch'));
// Run a preview server and watch for file changes.
gulp.task('serve:styleguide',
gulp.series(styleguideServer, 'watch'));
// Lint Sass and JavaScript sources.
gulp.task('lint',
gulp.parallel('lint:sass', 'lint:javascript'));
// An alias to the 'build' task.
gulp.task('default',
gulp.parallel('build'));
/// Internal tasks ------------------------------------------------------------
// Start a server with BrowserSync and proxify the application in.
function proxyServer(done) {
browser.init({
proxy: CONFIG.SERVER_PROXY,
port: CONFIG.SERVER_PORT,
serveStatic: [
{
route: '/static',
dir: CONFIG.BUILD_PATH
},
{
route: '/styleguide',
dir: './styleguide'
}
],
ghostMode: false,
notify: false
});
done();
}
// Start a server with BrowserSync with the styleguide.
function styleguideServer(done) {
browser.init({
server: {
baseDir: './styleguide',
routes: {
'/static': CONFIG.BUILD_PATH
}
},
port: CONFIG.SERVER_PORT,
ghostMode: false,
notify: false
});
done();
}
// Reload the BrowserSync server.
function reload(done) {
browser.reload();
done();
}

1
gvot/__init__.py Normal file
Voir le fichier

@ -0,0 +1 @@
__version__ = '0.1.0'

1
gvot/base/__init__.py Normal file
Voir le fichier

@ -0,0 +1 @@
default_app_config = 'gvot.base.apps.BaseConfig'

6
gvot/base/apps.py Normal file
Voir le fichier

@ -0,0 +1,6 @@
from django.apps import AppConfig
class BaseConfig(AppConfig):
name = 'gvot.base'
verbose_name = "Base"

93
gvot/base/block_utils.py Normal file
Voir le fichier

@ -0,0 +1,93 @@
from django.conf import settings
from django.utils.text import camel_case_to_spaces, slugify
from wagtail.core import blocks
from wagtail.documents.blocks import DocumentChooserBlock
from wagtail.images.blocks import ImageChooserBlock
# Mixins
########
class TemplatedBlock(blocks.StructBlock):
def get_template(self, *args, **kwargs):
return "blocks/{}.html".format(slugify(self.name))
class LinkMixin:
def links_blocklist():
return [
('page', blocks.PageChooserBlock(target_model='base.SitePage')),
('document', DocumentChooserBlock()),
('image', ImageChooserBlock()),
(
'lien_externe',
blocks.URLBlock(
icon='link', help_text="Lien vers un site externe."
),
),
(
'ancre',
blocks.TextBlock(
icon='fa fa-anchor',
help_text="Ancre dans la page. Ex : #infos",
),
),
]
def get_context(self, value, parent_context=None):
context = super().get_context(value, parent_context=parent_context)
lien = value['lien']
context['href'] = None
try:
if lien:
if hasattr(lien[0].value, 'file') and hasattr(
lien[0].value.file, 'url'
):
context['href'] = lien[0].value.file.url
elif hasattr(lien[0].value, 'url'):
context['href'] = lien[0].value.url
else:
context['href'] = lien[0].value
except Exception as e:
if settings.DEBUG:
raise e
if not context['href']:
context['href'] = '#'
return context
# Management
############
REGISTERED_BLOCKS = {}
def register_block():
"""
Macro hygiénique pour conserver une liste des blocs décrits,
groupés par type.
"""
class Wrapper:
def __init__(self, cls, *args, **kwargs):
self.wrapped = cls(*args, **kwargs)
title = camel_case_to_spaces(cls.__name__).replace(' ', '_')
group = cls._meta_class.group
if group not in REGISTERED_BLOCKS:
REGISTERED_BLOCKS[group] = []
REGISTERED_BLOCKS[group].append((title, cls))
def __call__(self, *args, **kwargs):
return self.wrapped
return Wrapper
def all_registered():
return [
(name, bloc())
for group in REGISTERED_BLOCKS
for name, bloc in REGISTERED_BLOCKS[group]
]

107
gvot/base/blocks.py Normal file
Voir le fichier

@ -0,0 +1,107 @@
from wagtail.core import blocks
from wagtail.embeds.blocks import EmbedBlock
from wagtail.images.blocks import ImageChooserBlock
from .block_utils import (
LinkMixin,
TemplatedBlock,
all_registered,
register_block,
)
# Contenu texte
# ^^^^^^^^^^^^^
@register_block()
class Titre(TemplatedBlock):
class Meta:
icon = 'title'
group = 'Contenu texte'
niveau = blocks.ChoiceBlock(
default='h2',
choices=[
('h{}'.format(i), 'Titre de niveau {}'.format(i))
for i in range(2, 7)
],
icon='title',
)
texte = blocks.TextBlock(icon='pilcrow')
@register_block()
class Paragraphe(TemplatedBlock):
class Meta:
icon = 'pilcrow'
group = 'Contenu texte'
texte = blocks.RichTextBlock(icon='pilcrow')
@register_block()
class Bouton(LinkMixin, TemplatedBlock):
class Meta:
label = "Bouton"
icon = 'link'
group = 'Contenu texte'
outline = blocks.BooleanBlock(
label="Bordures colorées uniquement", required=False, default=False
)
texte = blocks.TextBlock(icon='pilcrow')
lien = blocks.StreamBlock(
LinkMixin.links_blocklist(),
icon='link',
help_text="Lien associé au bouton ; optionnel",
required=False,
max_num=1,
)
# Contenu media
# ^^^^^^^^^^^^^
@register_block()
class Image(LinkMixin, TemplatedBlock):
class Meta:
icon = 'image'
group = 'Contenu média'
image = ImageChooserBlock(icon='image')
legende = blocks.RichTextBlock(
icon='pilcrow',
label="légende",
features=['ol', 'ul', 'bold', 'italic', 'link', 'document-link'],
required=False,
)
lien = blocks.StreamBlock(
LinkMixin.links_blocklist(),
icon='link',
help_text="Lien associé à l'image ; optionnel",
required=False,
max_num=1,
)
@register_block()
class Embarque(TemplatedBlock):
class Meta:
icon = 'media'
group = 'Contenu média'
label = 'média embarqué'
media = EmbedBlock(icon='link', help_text='Lien vers le média embarqué.')
# Contenu de page
# ^^^^^^^^^^^^^^^
def main_body_blocklist():
return all_registered()

Voir le fichier

@ -0,0 +1,27 @@
# Generated by Django 2.1.13 on 2019-10-11 15:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('wagtailcore', '0040_page_draft_title'),
]
operations = [
migrations.CreateModel(
name='SitePage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
],
options={
'verbose_name': 'page standard',
'verbose_name_plural': 'pages standard',
},
bases=('wagtailcore.page',),
),
]

Voir le fichier

@ -0,0 +1,55 @@
# Generated by Django 2.1.13 on 2019-10-11 16:02
from django.db import migrations
def reset_home_page(apps, schema_editor):
ContentType = apps.get_model('contenttypes.ContentType')
WagtailPage = apps.get_model('wagtailcore.Page')
Site = apps.get_model('wagtailcore.Site')
SitePage = apps.get_model('base.SitePage')
# Create page content type
sitepage_content_type, created = ContentType.objects.get_or_create(
model='sitepage',
app_label='base'
)
# Create home page
homepage = SitePage.objects.create(
title="Bienvenue sur votre nouveau site Wagtail !",
slug='homepage',
content_type=sitepage_content_type,
path='00010002',
depth=2,
numchild=0,
url_path='/',
)
# Update default site
site = Site.objects.get(is_default_site=True)
old_root = site.root_page
site.root_page_id=homepage.id
site.save()
# Delete unwanted root
wagtailpage_content_type = ContentType.objects.get(
model='page',
app_label='wagtailcore'
)
if old_root.content_type == wagtailpage_content_type:
old_root.delete()
class Migration(migrations.Migration):
dependencies = [
('wagtailadmin', '0001_create_admin_access_permissions'),
('base', '0001_initial'),
]
operations = [
migrations.RunPython(
reset_home_page,
),
]

Diff de fichier supprimé car une ou plusieurs lignes sont trop longues

Voir le fichier

@ -0,0 +1,69 @@
# Generated by Django 2.1.13 on 2020-02-20 17:53
import gvot.base.mixins
from django.db import migrations, models
import django.db.models.deletion
import modelcluster.fields
import wagtail.core.fields
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0041_group_collection_permissions_verbose_name_plural'),
('base', '0003_sitepage_body'),
]
operations = [
migrations.CreateModel(
name='FormField',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sort_order', models.IntegerField(blank=True, editable=False, null=True)),
('label', models.CharField(help_text='The label of the form field', max_length=255, verbose_name='label')),
('required', models.BooleanField(default=True, verbose_name='required')),
('choices', models.TextField(blank=True, help_text='Comma separated list of choices. Only applicable in checkboxes, radio and dropdown.', verbose_name='choices')),
('default_value', models.CharField(blank=True, help_text='Default value. Comma separated values supported for checkboxes.', max_length=255, verbose_name='default value')),
('help_text', models.CharField(blank=True, max_length=255, verbose_name='help text')),
('field_type', models.CharField(choices=[('singleline', 'Single line text'), ('multiline', 'Multi-line text'), ('email', 'Email'), ('number', 'Number'), ('url', 'URL'), ('checkbox', 'Checkbox'), ('checkboxes', 'Choix multiples'), ('dropdown', 'Drop down'), ('multiselect', 'Multiple select'), ('radio', 'Radio buttons'), ('date', 'Date')], max_length=16, verbose_name='field type')),
],
options={
'ordering': ['sort_order'],
'abstract': False,
},
),
migrations.CreateModel(
name='Formulaire',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
('to_address', models.CharField(blank=True, help_text='Optional - form submissions will be emailed to these addresses. Separate multiple addresses by comma.', max_length=255, verbose_name='to address')),
('from_address', models.CharField(blank=True, max_length=255, verbose_name='from address')),
('subject', models.CharField(blank=True, max_length=255, verbose_name='subject')),
('peremption', models.DateField(help_text='Uniquement destiné à avoir un point repère en vue de la suppression future des données', verbose_name='Date de péremption')),
('prescription', wagtail.core.fields.RichTextField(default='Ces données sont recueillies dans le seul but décrit en introduction du formulaire. Les données recueillies dans le cadre de cette campagne ne seront pas utilisées à d’autres fins ni transmises à un tiers. Vous disposez d’un droit d’accès, de modification, de rectification et de suppression des données vous concernant (loi « Informatique et Liberté » du 6 janvier 1978).', help_text="Texte destiné à avertir de l'utilisation qui sera faite des données recueillies.")),
('introduction', wagtail.core.fields.RichTextField(blank=True)),
('action', models.TextField(default='Envoyer', help_text='Texte du bouton du formulaire.')),
('confirmation', wagtail.core.fields.RichTextField(blank=True)),
],
options={
'abstract': False,
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='FormulaireIndex',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
],
options={
'verbose_name': 'liste des formulaires',
'verbose_name_plural': 'listes des formulaires',
},
bases=(gvot.base.mixins.UniqPage, 'wagtailcore.page'),
),
migrations.AddField(
model_name='formfield',
name='page',
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='form_fields', to='base.Formulaire'),
),
]

Voir le fichier

5
gvot/base/mixins.py Normal file
Voir le fichier

@ -0,0 +1,5 @@
class UniqPage:
@classmethod
def can_create_at(cls, parent):
# Seulement une instance possible
return not cls.objects.exists() and super().can_create_at(parent)

161
gvot/base/models.py Normal file
Voir le fichier

@ -0,0 +1,161 @@
from django.db import models
from django.http import Http404
from modelcluster.fields import ParentalKey
from wagtail.admin.edit_handlers import (
FieldPanel,
FieldRowPanel,
InlinePanel,
MultiFieldPanel,
StreamFieldPanel,
)
from wagtail.contrib.forms.edit_handlers import FormSubmissionsPanel
from wagtail.contrib.forms.models import (
FORM_FIELD_CHOICES,
AbstractEmailForm,
AbstractFormField,
)
from wagtail.core.fields import RichTextField, StreamField
from wagtail.core.models import Page
from wagtail.search import index
from . import blocks, mixins
class SitePage(Page):
"""
La page générique de base du site web.
"""
class Meta:
verbose_name = "page standard"
verbose_name_plural = "pages standard"
# Contraintes de structure
# ------------------------
parent_page_types = ['SitePage', Page]
subpage_types = ['SitePage', 'FormulaireIndex']
# Contenu
# -------
body = StreamField(
blocks.main_body_blocklist(),
verbose_name="contenu",
null=True,
blank=True,
)
content_panels = Page.content_panels + [StreamFieldPanel('body')]
search_fields = Page.search_fields + [
index.SearchField('body'),
]
class FormulaireIndex(mixins.UniqPage, Page):
"""
Elle sert à lister les formulaires. Elle n'a a priori aucun intérêt à
apparaître au public.
"""
class Meta:
verbose_name = "liste des formulaires"
verbose_name_plural = "listes des formulaires"
parent_page_types = ['SitePage', Page]
subpage_types = ['Formulaire']
def serve(self, request):
raise Http404
# Override the field_type field with personnalized choices
FORM_FIELD_CHOICES = [
(c[0], 'Choix multiples') if c[0] == 'checkboxes' else c
for c in FORM_FIELD_CHOICES
if c[0] not in ['datetime', 'hidden']
]
class FormField(AbstractFormField):
"""
Classe des champs de formulaire.
"""
page = ParentalKey(
'Formulaire', on_delete=models.CASCADE, related_name='form_fields'
)
field_type = models.CharField(
verbose_name='field type', max_length=16, choices=FORM_FIELD_CHOICES,
)
class Formulaire(AbstractEmailForm):
"""
Elle sert à publier un formulaire pour une inscription à un évènement,
une newsletter, etc. ou n'importe quelle récolte de données simples.
"""
parent_page_types = ['FormulaireIndex']
subpage_types = []
peremption = models.DateField(
"Date de péremption",
help_text="Uniquement destiné à avoir un point repère en vue de "
"la suppression future des données",
)
prescription = RichTextField(
default="Ces données sont recueillies dans le seul but "
"décrit en introduction du formulaire. Les données "
"recueillies dans le cadre de cette campagne ne seront pas "
"utilisées à d’autres fins ni transmises à un tiers. Vous "
"disposez d’un droit d’accès, de modification, de "
"rectification et de suppression des données vous "
"concernant (loi « Informatique et Liberté » du "
"6 janvier 1978).",
help_text="Texte destiné à avertir de l'utilisation qui sera faite "
"des données recueillies.",
)
introduction = RichTextField(blank=True)
action = models.TextField(
default="Envoyer", help_text="Texte du bouton du formulaire.",
)
confirmation = RichTextField(blank=True)
# FIXME: mettre les questions dans un panel séparé
content_panels = AbstractEmailForm.content_panels + [
FormSubmissionsPanel(),
MultiFieldPanel(
[
FieldPanel('peremption'),
FieldPanel('prescription'),
], "Aspects RGPD",
),
FieldPanel('introduction'),
InlinePanel('form_fields', label="Champs de formulaire"),
FieldPanel('confirmation'),
MultiFieldPanel(
[
FieldPanel('action'),
], "Appel à action"
),
MultiFieldPanel(
[
FieldRowPanel(
[
FieldPanel('from_address'),
FieldPanel('to_address'),
]
),
FieldPanel('subject'),
],
"Envoi des résultats",
),
]
search_fields = Page.search_fields + [
index.SearchField('introduction'),
]

Voir le fichier

Voir le fichier

@ -0,0 +1,33 @@
import functools
import os.path
from django import template
from django.conf import settings
from django.contrib.staticfiles import finders
from django.templatetags.static import static
register = template.Library()
@functools.lru_cache(maxsize=None)
def get_minified_static_path(path):
"""Retourne de préférence le chemin d'un fichier compressé.
Détermine et retourne le chemin relatif à utiliser pour le fichier
statique `path`, en fonction de l'environnement. Si elle existe, la
version compressée (e.g. avec le suffixe `.min` avant l'extension) du
fichier sera retournée quand le débogage est désactivé.
"""
if settings.DEBUG:
return path
root, ext = os.path.splitext(path)
min_path = '{}.min{}'.format(root, ext or '')
if finders.find(min_path):
return min_path
return path
@register.simple_tag
def minified(path):
"""Retourne le chemin absolu d'un fichier statique compressé."""
return static(get_minified_static_path(path))

Voir le fichier

Voir le fichier

@ -0,0 +1,15 @@
import pytest
from wagtail.documents.models import get_document_model
from wagtail.images.tests.utils import Image, get_test_image_file
@pytest.fixture
def document(django_db_setup, django_db_blocker):
return get_document_model().objects.create(
title="Test document", file=get_test_image_file()
)
@pytest.fixture
def image(django_db_setup, django_db_blocker):
return Image.objects.create(title="Test image", file=get_test_image_file())

Voir le fichier

@ -0,0 +1,192 @@
import pytest
from .. import blocks
class TestBouton:
def setup(self):
self.block = blocks.Bouton()
self.block.name = 'bouton'
def test_bouton(self):
data = {'outline': False, 'texte': "Bouton"}
html = self.block.render(self.block.to_python(data))
assert '<a class="btn btn-primary"' in html
assert '</a>' in html
assert "Bouton" in html
def test_bouton_outline(self):
data = {'outline': True, 'texte': "Bouton"}
html = self.block.render(self.block.to_python(data))
assert '<a class="btn btn-outline-primary"' in html
assert '</a>' in html
assert "Bouton" in html
@pytest.mark.django_db
class TestBoutonHref(TestBouton):
def test_bouton_bad_page(self):
data = {'texte': "Bouton", 'lien': [{'type': 'page', 'value': 42}]}
html = self.block.render(self.block.to_python(data))
assert 'href="#"' in html
def test_bouton_page(self):
data = {'texte': "Bouton", 'lien': [{'type': 'page', 'value': 3}]}
html = self.block.render(self.block.to_python(data))
assert 'href="/"' in html
def test_bouton_image(self, image):
data = {
'texte': "Bouton",
'lien': [{'type': 'image', 'value': image.id}],
}
html = self.block.render(self.block.to_python(data))
assert 'href="{}"'.format(image.file.url) in html
def test_bouton_document(self, document):
data = {
'texte': "Bouton",
'lien': [{'type': 'document', 'value': document.id}],
}
html = self.block.render(self.block.to_python(data))
assert 'href="{}"'.format(document.file.url) in html
def test_bouton_externe(self):
data = {
'texte': "Bouton",
'lien': [{'type': 'lien_externe', 'value': 'https://april.org'}],
}
html = self.block.render(self.block.to_python(data))
assert 'href="https://april.org"' in html
def test_bouton_ancre(self):
data = {
'texte': "Bouton",
'lien': [{'type': 'ancre', 'value': '#blang'}],
}
html = self.block.render(self.block.to_python(data))
assert 'href="#blang"' in html
class TestBoutonExcept(TestBouton):
def test_get_context_pass(self):
context = self.block.get_context({'lien': None})
assert context['href'] == '#'
def test_get_context_except(self, settings):
settings.DEBUG = True
with pytest.raises(Exception):
self.block.get_context({'lien': True})
@pytest.mark.django_db
class TestImage:
def setup(self):
self.block = blocks.Image()
self.block.name = 'image'
def test_image(self, image):
data = {'image': image.id}
html = self.block.render(self.block.to_python(data))
assert '<a' not in html
assert '</a>' not in html
assert 'figcaption' not in html
assert 'alt="{}"'.format(image.title) in html
assert (
'src="{}"'.format(image.get_rendition('max-1200x1200').url) in html
)
def test_image_legende(self, image):
data = {'image': image.id, 'legende': "Légende de l'image"}
html = self.block.render(self.block.to_python(data))
assert '<a' not in html
assert '</a>' not in html
assert 'figcaption' in html
assert data['legende'] in html
assert 'alt="{}"'.format(image.title) in html
assert (
'src="{}"'.format(image.get_rendition('max-1200x1200').url) in html
)
def test_image_bad_page(self, image):
data = {'image': image.id, 'lien': [{'type': 'page', 'value': 42}]}
html = self.block.render(self.block.to_python(data))
assert '<a href="#"' in html
def test_image_page(self, image):
data = {'image': image.id, 'lien': [{'type': 'page', 'value': 3}]}
html = self.block.render(self.block.to_python(data))
assert '<a href="/"' in html
def test_image_image(self, image):
data = {
'image': image.id,
'lien': [{'type': 'image', 'value': image.id}],
}
html = self.block.render(self.block.to_python(data))
assert '<a href="{}"'.format(image.file.url) in html
def test_image_document(self, document, image):
data = {
'image': image.id,
'lien': [{'type': 'document', 'value': document.id}],
}
html = self.block.render(self.block.to_python(data))
assert '<a href="{}"'.format(document.file.url) in html
def test_image_externe(self, image):
data = {
'image': image.id,
'lien': [{'type': 'lien_externe', 'value': 'https://april.org'}],
}
html = self.block.render(self.block.to_python(data))
assert '<a href="https://april.org"' in html
def test_image_ancre(self, image):
data = {
'image': image.id,
'lien': [{'type': 'ancre', 'value': '#blang'}],
}
html = self.block.render(self.block.to_python(data))
assert '<a href="#blang"' in html
class TestParagraphe:
def test_paragraphe(self):
block = blocks.Paragraphe()
block.name = 'paragraphe'
data = {'texte': 'Texte du paragraphe'}
html = block.render(block.to_python(data))
assert data['texte'] in html
class TestTitre:
def test_titre(self):
block = blocks.Titre()
block.name = 'titre'
data = {'niveau': 'h3', 'texte': "Titre de niveau 3"}
html = block.render(block.to_python(data))
assert '<h3>' in html
assert '</h3>' in html
assert "Titre de niveau 3" in html
assert '<a class="anchor" id="titre-de-niveau-3"' in html

Voir le fichier

@ -0,0 +1,46 @@
from django.contrib.auth.models import User
import pytest
from wagtail.core.models import Page, Site
from ..models import SitePage
@pytest.mark.django_db
class TestSitePage:
def test_draft_creation(self):
owner = User.objects.get_or_create(username="Anne")[0]
instance = SitePage(live=False, owner=owner, title="title")
assert instance
def test_add_draft_in_arborescence(self):
owner = User.objects.get_or_create(username="Anne")[0]
instance = SitePage(live=False, owner=owner, title="title")
assert not instance.path
site = Site.objects.get(is_default_site=True)
site.root_page.add_child(instance=instance)
instance.save_revision(user=owner)
assert instance.path
assert site.root_page.get_children().count()
assert not site.root_page.get_children().live().count()
def test_publish_in_arborescence(self):
owner = User.objects.get_or_create(username="Anne")[0]
instance = SitePage(live=False, owner=owner, title="title")
assert not instance.path
site = Site.objects.get(is_default_site=True)
site.root_page.add_child(instance=instance)
instance.save_revision(user=owner)
instance.revisions.last().publish()
assert instance.path
assert site.root_page.get_children().live().count()
def test_no_more_wagtail_page_in_arborescence(self):
site = Site.objects.get(is_default_site=True)
owner = User.objects.get_or_create(username="Anne")[0]
instance = Page(live=False, owner=owner, title="title")
assert not instance.can_create_at(site.root_page)

Voir le fichier

@ -0,0 +1,43 @@
from django.template import Context, Template
import pytest
from ..templatetags.minified import get_minified_static_path
@pytest.fixture
def mock_static_find(monkeypatch):
def find(*args, **kwargs):
return True
# patch pour trouver n'importe quel fichier dans les statics
monkeypatch.setattr('django.contrib.staticfiles.finders.find', find)
class TestMinified:
def setup(self):
# vide le cache avant chaque test
get_minified_static_path.cache_clear()
def test_get_path_debug(self, mock_static_find, settings):
settings.DEBUG = True
assert get_minified_static_path('test/debug.css') == 'test/debug.css'
@pytest.mark.parametrize(
'path, result',
[
('test/app.css', 'test/app.min.css'),
('test/no_extension', 'test/no_extension.min'),
],
)
def test_get_path_exists(self, mock_static_find, path, result):
assert get_minified_static_path(path) == result
def test_get_path_not_found(self):
assert get_minified_static_path('unknown.txt') == 'unknown.txt'
def test_tag(self, mock_static_find, settings):
rendered = Template(
'{% load minified %}{% minified "test/tag.css" %}'
).render(Context())
assert rendered == settings.STATIC_URL + 'test/tag.min.css'

Voir le fichier

@ -0,0 +1,61 @@
from django.urls import reverse
import pytest
from wagtail.core.models import Site
from ..models import SitePage
from .utils import count_text_in_content
@pytest.mark.django_db
class TestSitePage:
def setup(self):
assert SitePage.objects.count() == 1
site = Site.objects.get(is_default_site=True)
self.page_id = site.root_page.id
self.data = {
'title': "Titre de la page",
'slug': 'testpage',
'body': '[]',
'body-count': 0,
}
def test_create_form(self, admin_client):
url = reverse(
'wagtailadmin_pages:add', args=['base', 'sitepage', self.page_id]
)
response = admin_client.get(url)
assert response.status_code == 200
def test_create_and_publish(self, admin_client):
url = reverse(
'wagtailadmin_pages:add', args=['base', 'sitepage', self.page_id]
)
self.data['action-publish'] = True
response = admin_client.post(url, self.data)
assert response.status_code == 302
assert SitePage.objects.count() == 2
def test_display_published(self, admin_client):
self.test_create_and_publish(admin_client)
url = SitePage.objects.last().full_url
response = admin_client.get(url)
assert response.status_code == 200
assert count_text_in_content(response, "Titre de la page")
def test_preview(self, admin_client):
url = reverse(
'wagtailadmin_pages:preview_on_add',
args=['base', 'sitepage', self.page_id],
)
response = admin_client.post(url, self.data)
assert response.status_code == 200
assert response.json().get('is_valid', False)
response = admin_client.get(url)
assert response.status_code == 200
assert count_text_in_content(response, "Titre de la page")
assert SitePage.objects.count() == 1

29
gvot/base/tests/utils.py Normal file
Voir le fichier

@ -0,0 +1,29 @@
from django.test.html import parse_html
def parse_content(response, html=False):
"""
Décode et retourne le contenu de la réponse, en le transformant en
objet de structure Python si `html` vaut `True`.
"""
if (
hasattr(response, "render")
and callable(response.render)
and not response.is_rendered
):
response.render()
content = response.content.decode(response.charset)
return parse_html(content) if html else content
def count_text_in_content(response, text, html=False):
"""
Retourne le nombre d'occurrence de `text` dans le contenu de la réponse,
en les analysant en tant que HTML si `html` vaut `True`.
Cette méthode se base sur `django.test.SimpleTestCase.assertContains()`,
et pourrait être remplacée après l'intégration de pytest-django#709.
"""
content = parse_content(response, html=html)
text = parse_html(text) if html else str(text)
return content.count(text)

Voir le fichier

@ -0,0 +1,13 @@
from django.utils.html import format_html
from wagtail.core import hooks
from .templatetags.minified import minified
@hooks.register('insert_global_admin_css')
def global_admin_css():
"""Ajoute une feuille de styles personnalisée dans l'admin."""
return format_html(
'<link rel="stylesheet" href="{}">', minified('css/admin.css')
)

25
gvot/settings/__init__.py Normal file
Voir le fichier

@ -0,0 +1,25 @@
import environ
"""The default environment to use."""
DEFAULT_ENVIRONMENT = 'production'
"""The environment variables of the app instance."""
env = environ.Env()
"""Path to the package root - e.g. Django project."""
root_dir = environ.Path(__file__) - 2
"""Path to the base directory of the app instance."""
base_dir = env.path('BASE_DIR', default=str(root_dir - 1))
# Load config.env, OS environment variables will take precedence
if env.bool('READ_CONFIG_FILE', default=True):
env.read_env(str(base_dir.path('config.env')))
"""The Django settings module's name to use."""
DJANGO_SETTINGS_MODULE = env(
'DJANGO_SETTINGS_MODULE',
default='gvot.settings.{}'.format(
env('ENV', default=DEFAULT_ENVIRONMENT)
),
)

261
gvot/settings/base.py Normal file
Voir le fichier

@ -0,0 +1,261 @@
"""
Django settings for GvoT project.
For more information on this file, see
https://docs.djangoproject.com/en/stable/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/stable/ref/settings/
"""
import os.path
from email.utils import getaddresses
from . import base_dir, env, root_dir
from .gvot import *
from .gvot import *
# ENVIRONMENT VARIABLES AND PATHS
# ------------------------------------------------------------------------------
# Local directory used for static and templates overrides
local_dir = base_dir.path('local')
# Directory for variable stuffs, i.e. user-uploaded media
var_dir = base_dir.path('var')
if not os.path.isdir(var_dir()):
os.mkdir(var_dir(), mode=0o755)
# Location on which the application is served
APP_LOCATION = env('APP_LOCATION', default='/')
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/ref/settings/#debug
DEBUG = env.bool('DJANGO_DEBUG', default=True)
# Local time zone for this installation
TIME_ZONE = 'Europe/Paris'
# https://docs.djangoproject.com/en/stable/ref/settings/#language-code
LANGUAGE_CODE = 'fr'
# https://docs.djangoproject.com/en/dev/ref/settings/#site-id
SITE_ID = 1
# https://docs.djangoproject.com/en/stable/ref/settings/#use-i18n
USE_I18N = True
# https://docs.djangoproject.com/en/stable/ref/settings/#use-l10n
USE_L10N = True
# https://docs.djangoproject.com/en/stable/ref/settings/#use-tz
USE_TZ = True
# DATABASES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
# https://django-environ.readthedocs.io/en/stable/#supported-types
DATABASES = {
'default': env.db(
'DJANGO_DATABASE_URL',
default='sqlite:///{}'.format(base_dir('sqlite.db')),
)
}
# URLS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/ref/settings/#root-urlconf
ROOT_URLCONF = 'gvot.urls'
# https://docs.djangoproject.com/en/stable/ref/settings/#wsgi-application
WSGI_APPLICATION = 'gvot.wsgi.application'
# APP CONFIGURATION
# ------------------------------------------------------------------------------
DJANGO_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
WAGTAIL_APPS = [
'wagtail.contrib.forms',
'wagtail.contrib.redirects',
'wagtail.embeds',
'wagtail.sites',
'wagtail.users',
'wagtail.snippets',
'wagtail.documents',
'wagtail.images',
'wagtail.search',
'wagtail.admin',
'wagtail.core',
'modelcluster',
'taggit',
]
# Project applications
LOCAL_APPS = ['gvot.base']
# https://docs.djangoproject.com/en/stable/ref/settings/#installed-apps
INSTALLED_APPS = DJANGO_APPS + WAGTAIL_APPS + THIRD_PARTY_APPS + LOCAL_APPS
# PASSWORDS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/ref/settings/#password-hashers
PASSWORD_HASHERS = [
# https://docs.djangoproject.com/en/stable/topics/auth/passwords/#using-argon2-with-django
# 'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.BCryptPasswordHasher',
]
# https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': (
'django.contrib.auth.password_validation.'
'UserAttributeSimilarityValidator'
)
},
{
'NAME': (
'django.contrib.auth.password_validation.MinimumLengthValidator'
)
},
{
'NAME': (
'django.contrib.auth.password_validation.'
'CommonPasswordValidator'
)
},
{
'NAME': (
'django.contrib.auth.password_validation.'
'NumericPasswordValidator'
)
},
]
# MIDDLEWARE
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/ref/settings/#middleware
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'wagtail.core.middleware.SiteMiddleware',
'wagtail.contrib.redirects.middleware.RedirectMiddleware',
]
# STATIC
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/ref/settings/#static-files
STATIC_ROOT = var_dir('static')
# https://docs.djangoproject.com/en/stable/ref/settings/#static-url
STATIC_URL = os.path.join(APP_LOCATION, 'static/')
# https://docs.djangoproject.com/en/stable/ref/settings/#staticfiles-dirs
STATICFILES_DIRS = [root_dir('static')]
if os.path.isdir(local_dir('static')):
STATICFILES_DIRS.insert(0, local_dir('static'))
# https://docs.djangoproject.com/en/stable/ref/contrib/staticfiles/#staticfiles-finders
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
# MEDIA
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/ref/settings/#media-root
MEDIA_ROOT = var_dir('media')
# https://docs.djangoproject.com/en/stable/ref/settings/#media-url
MEDIA_URL = os.path.join(APP_LOCATION, 'media/')
# https://docs.djangoproject.com/en/stable/ref/settings/#file-upload-directory-permissions
FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755
# https://docs.djangoproject.com/en/stable/ref/settings/#file-upload-permissions
FILE_UPLOAD_PERMISSIONS = 0o644
# TEMPLATES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/ref/settings/#templates
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [root_dir('templates')],
'OPTIONS': {
'debug': DEBUG,
'loaders': [
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
],
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
],
},
}
]
if os.path.isdir(local_dir('templates')):
TEMPLATES[0]['DIRS'].insert(0, local_dir('templates'))
# FIXTURES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/ref/settings/#fixture-dirs
FIXTURE_DIRS = [root_dir('fixtures')]
# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/topics/email/#email-backends
# https://django-environ.readthedocs.io/en/stable/#supported-types
vars().update(env.email_url('DJANGO_EMAIL_URL', default='smtp://localhost:25'))
DEFAULT_FROM_EMAIL = env('DEFAULT_FROM_EMAIL', default='webmaster@localhost')
# Use the same email address for error messages
SERVER_EMAIL = DEFAULT_FROM_EMAIL
# ADMIN
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/ref/settings/#admins
ADMINS = getaddresses([env('ADMINS', default='Cliss XXI <francois.poulain@cliss21.org>')])
# https://docs.djangoproject.com/en/stable/ref/settings/#managers
MANAGERS = ADMINS
# SESSIONS AND COOKIES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/ref/settings/#session-cookie-path
SESSION_COOKIE_PATH = APP_LOCATION
# https://docs.djangoproject.com/en/stable/ref/settings/#csrf-cookie-path
CSRF_COOKIE_PATH = APP_LOCATION
# WAGTAIL
# ------------------------------------------------------------------------------
# http://docs.wagtail.io/en/stable/advanced_topics/settings.html
WAGTAIL_SITE_NAME = "GvoT"
# Disable Gravatar provider
WAGTAIL_GRAVATAR_PROVIDER_URL = None
# Disable update checking on the dashboard
WAGTAIL_ENABLE_UPDATE_CHECK = False
# ------------------------------------------------------------------------------
# APPLICATION AND 3RD PARTY LIBRARY SETTINGS
# ------------------------------------------------------------------------------

Voir le fichier

@ -0,0 +1,50 @@
"""
Development settings.
- use Console backend for emails sending by default
- add the django-debug-toolbar
"""
from .base import * # noqa
from .base import INSTALLED_APPS, MIDDLEWARE, env
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/ref/settings/#secret-key
SECRET_KEY = env('DJANGO_SECRET_KEY', default='CHANGEME!!!')
# https://docs.djangoproject.com/en/stable/ref/settings/#allowed-hosts
ALLOWED_HOSTS = env.list(
'DJANGO_ALLOWED_HOSTS', default=['localhost', '0.0.0.0', '127.0.0.1']
)
# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/topics/email/#email-backends
# https://django-environ.readthedocs.io/en/stable/#supported-types
vars().update(env.email_url('DJANGO_EMAIL_URL', default='consolemail://'))
# WAGTAIL
# ------------------------------------------------------------------------------
# http://docs.wagtail.io/en/stable/contributing/styleguide.html
INSTALLED_APPS += ['wagtail.contrib.styleguide']
# ------------------------------------------------------------------------------
# APPLICATION AND 3RD PARTY LIBRARY SETTINGS
# ------------------------------------------------------------------------------
# DJANGO DEBUG TOOLBAR
# ------------------------------------------------------------------------------
# https://django-debug-toolbar.readthedocs.io/en/stable/installation.html
if env.bool('DJANGO_DEBUG_TOOLBAR', default=False):
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
INSTALLED_APPS += ['debug_toolbar']
INTERNAL_IPS = ['127.0.0.1']
DEBUG_TOOLBAR_CONFIG = {
'DISABLE_PANELS': ['debug_toolbar.panels.redirects.RedirectsPanel'],
'SHOW_TEMPLATE_CONTEXT': True,
}
# DJANGO EXTENSIONS
# ------------------------------------------------------------------------------
# https://django-extensions.readthedocs.io/en/stable/index.html
INSTALLED_APPS += ['django_extensions']

15
gvot/settings/gvot.py Normal file
Voir le fichier

@ -0,0 +1,15 @@
"""
Django specific settings for GvoT project.
"""
THIRD_PARTY_APPS = [
'wagtailmenus',
'widget_tweaks',
]
WAGTAILMENUS_FLAT_MENUS_HANDLE_CHOICES = (('footer', 'Menu de pied de page'),)
WAGTAILEMBEDS_FINDERS = [
{'class': 'wagtail.embeds.finders.oembed'},
{'class': 'wagtailembedpeertube.finders'},
]

106
gvot/settings/production.py Normal file
Voir le fichier

@ -0,0 +1,106 @@
"""
Production settings.
- validate the configuration
- disable debug mode
- load secret key from environment variables
- set other production configurations
"""
import os
from django.core.exceptions import ImproperlyConfigured
from .base import * # noqa
from .base import TEMPLATES, env, var_dir
# CONFIGURATION VALIDATION
# ------------------------------------------------------------------------------
# Ensure that the database configuration has been set
if not env('DJANGO_DATABASE_URL', default=None):
raise ImproperlyConfigured(
"No database configuration has been set, you should check "
"the value of your DATABASE_URL environment variable."
)
# Ensure that the default email address has been set
if not env('DEFAULT_FROM_EMAIL', default=None):
raise ImproperlyConfigured(
"No default email address has been set, you should check "
"the value of your DEFAULT_FROM_EMAIL environment variable."
)
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/ref/settings/#debug
DEBUG = False
# https://docs.djangoproject.com/en/stable/ref/settings/#secret-key
SECRET_KEY = env('DJANGO_SECRET_KEY')
# https://docs.djangoproject.com/en/stable/ref/settings/#allowed-hosts
ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS', default=[])
# TEMPLATES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/ref/settings/#templates
TEMPLATES[0]['OPTIONS']['debug'] = DEBUG
TEMPLATES[0]['OPTIONS']['loaders'] = [
(
'django.template.loaders.cached.Loader',
[
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
],
)
]
# LOGGING
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/stable/topics/logging/
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '%(asctime)s - %(levelname)s - %(module)s: %(message)s'
}
},
'handlers': {
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
},
'file': {
'level': 'DEBUG',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': var_dir('log/gvot.log'),
'formatter': 'verbose',
'when': 'midnight',
'interval': 1,
'backupCount': 30,
},
},
'loggers': {
'django': {
'level': 'WARNING',
'handlers': ['file'],
'propagate': True,
},
'django.request': {
'level': 'WARNING',
'handlers': ['file', 'mail_admins'],
'propagate': True,
},
'gvot': {
'level': 'INFO',
'handlers': ['file', 'mail_admins'],
'propagate': True,
},
},
}
if not os.path.isdir(var_dir('log')):
os.mkdir(var_dir('log'), mode=0o750)
# ------------------------------------------------------------------------------
# APPLICATION AND 3RD PARTY LIBRARY SETTINGS
# ------------------------------------------------------------------------------

Certains fichiers ne sont pas affichés car ce diff contient trop de modifications Voir plus