build(init): initialisation depuis le cookiecutter wagtail
révision
e790585c3a
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": [ "@babel/preset-env" ]
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
gvot/static/** -diff
|
||||||
|
assets/img/** -diff
|
||||||
|
assets/fonts/** -diff
|
|
@ -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/
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
This software is developped by Cliss XXI.
|
Fichier diff supprimé car celui-ci est trop grand
Voir la Diff
|
@ -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
|
|
@ -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).
|
|
@ -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();
|
||||||
|
});
|
|
@ -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';
|
|
@ -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
|
||||||
|
);
|
|
@ -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";
|
|
@ -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";
|
|
@ -0,0 +1,3 @@
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Font faces declarations
|
||||||
|
// -----------------------------------------------------------------------------
|
|
@ -0,0 +1,9 @@
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Forms component's extension
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Indicate that a form field is required.
|
||||||
|
.required {
|
||||||
|
font-size: 90%;
|
||||||
|
color: $danger;
|
||||||
|
}
|
|
@ -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";
|
|
@ -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";
|
|
@ -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
|
|
@ -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();
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
__version__ = '0.1.0'
|
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'gvot.base.apps.BaseConfig'
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BaseConfig(AppConfig):
|
||||||
|
name = 'gvot.base'
|
||||||
|
verbose_name = "Base"
|
|
@ -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]
|
||||||
|
]
|
|
@ -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()
|
|
@ -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',),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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)
|
|
@ -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'),
|
||||||
|
]
|
|
@ -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))
|
|
@ -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())
|
|
@ -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
|
|
@ -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)
|
|
@ -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'
|
|
@ -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
|
|
@ -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)
|
|
@ -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')
|
||||||
|
)
|
|
@ -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)
|
||||||
|
),
|
||||||
|
)
|
|
@ -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
|
||||||
|
# ------------------------------------------------------------------------------
|
|
@ -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']
|
|
@ -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'},
|
||||||
|
]
|
|
@ -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
Chargement…
Référencer dans un nouveau ticket