Compare commits
218 Commits
main
...
wip/python
Author | SHA1 | Date |
---|---|---|
Frédéric Péters | 094cae52e5 | |
Frédéric Péters | 00e130ae53 | |
Frédéric Péters | b5c262dc64 | |
Frédéric Péters | 731697ebc9 | |
Frédéric Péters | cb17e126b7 | |
Frédéric Péters | a87a181371 | |
Frédéric Péters | 7a29de02bb | |
Frédéric Péters | f1b90ce1e2 | |
Frédéric Péters | 57f96ee3b4 | |
Frédéric Péters | 7b0978cf00 | |
Frédéric Péters | 4b9c230141 | |
Frédéric Péters | 4f3dc60311 | |
Frédéric Péters | 4f488c1d57 | |
Frédéric Péters | 9cf9355463 | |
Frédéric Péters | 03987a6e6c | |
Frédéric Péters | 625b574280 | |
Frédéric Péters | 7dcd2d485b | |
Frédéric Péters | 52df31918f | |
Frédéric Péters | 4aa96b93c5 | |
Frédéric Péters | 0be6a94f13 | |
Frédéric Péters | 0eb5415bee | |
Frédéric Péters | e9771cc834 | |
Frédéric Péters | a1414e1ebc | |
Frédéric Péters | 478ff982a1 | |
Frédéric Péters | 82fd9489bf | |
Frédéric Péters | 88d47903d1 | |
Frédéric Péters | a923e056ea | |
Frédéric Péters | 2877790b24 | |
Frédéric Péters | 3145fd7367 | |
Frédéric Péters | 1bb05944f8 | |
Frédéric Péters | 54d4b4686b | |
Frédéric Péters | 21dd276479 | |
Frédéric Péters | 12094de8e4 | |
Frédéric Péters | 859683f690 | |
Frédéric Péters | a55daeec35 | |
Frédéric Péters | b48283f626 | |
Frédéric Péters | 30a9d2d4c3 | |
Frédéric Péters | e45c19e794 | |
Frédéric Péters | badde34b2e | |
Frédéric Péters | 191f01bd7e | |
Frédéric Péters | 762560fcaf | |
Frédéric Péters | 228053c9ce | |
Frédéric Péters | 158d6fac2d | |
Frédéric Péters | 207d3ae91e | |
Frédéric Péters | fdb0ad1aa1 | |
Frédéric Péters | 1e8ca52e3c | |
Frédéric Péters | 00fc3deef0 | |
Frédéric Péters | f4ad46acc6 | |
Frédéric Péters | 370357e40a | |
Frédéric Péters | 3fefe4a344 | |
Frédéric Péters | 78084aa023 | |
Frédéric Péters | 7a446f5c88 | |
Frédéric Péters | 1591530889 | |
Frédéric Péters | 8e52fc8814 | |
Frédéric Péters | b3bc038a2e | |
Frédéric Péters | e0e128de4b | |
Frédéric Péters | 1ec58ee869 | |
Frédéric Péters | 96bf98cd7c | |
Frédéric Péters | a33e8ff8b4 | |
Frédéric Péters | 6875bf8954 | |
Frédéric Péters | 72bd7af167 | |
Frédéric Péters | dabe208677 | |
Frédéric Péters | 5793e5c509 | |
Frédéric Péters | 8158b456fa | |
Frédéric Péters | d62e945441 | |
Frédéric Péters | 9b2e2c8d13 | |
Frédéric Péters | ed9cdb777c | |
Frédéric Péters | 844dea5853 | |
Frédéric Péters | 31cc721510 | |
Frédéric Péters | 8c1fc39764 | |
Frédéric Péters | b401245d4b | |
Frédéric Péters | 87e940ddfc | |
Frédéric Péters | 248e7563c7 | |
Frédéric Péters | 9f9f623cb4 | |
Frédéric Péters | a68dccf8dd | |
Frédéric Péters | dc82d0f918 | |
Frédéric Péters | 21ee4427c6 | |
Frédéric Péters | b1fa54eb33 | |
Frédéric Péters | 6c89239f3d | |
Frédéric Péters | 129306e47d | |
Frédéric Péters | 85a4024417 | |
Frédéric Péters | 091ba4ae15 | |
Frédéric Péters | 81f2f7d90c | |
Frédéric Péters | 7d329c856b | |
Frédéric Péters | d0b9ff274c | |
Frédéric Péters | a4b8063976 | |
Frédéric Péters | 5a7161bbb5 | |
Frédéric Péters | fca37a3205 | |
Frédéric Péters | 4c6c08dcae | |
Frédéric Péters | 10d4451122 | |
Frédéric Péters | 3444d4bec8 | |
Frédéric Péters | d0b2407738 | |
Frédéric Péters | 5f8c243cd0 | |
Frédéric Péters | 7523343893 | |
Frédéric Péters | c3057f76bd | |
Frédéric Péters | ca35deba02 | |
Frédéric Péters | c089fbe70c | |
Frédéric Péters | 937b2c538d | |
Frédéric Péters | 4f7d4cd6cf | |
Frédéric Péters | 75c18872dc | |
Frédéric Péters | 95d29ec088 | |
Frédéric Péters | 03f2634895 | |
Frédéric Péters | 3938a8ab87 | |
Frédéric Péters | d1a7e6e187 | |
Frédéric Péters | b97f9a63f0 | |
Frédéric Péters | 1356365a15 | |
Frédéric Péters | 2cd20e666d | |
Frédéric Péters | 48118f8781 | |
Frédéric Péters | 4322fe9f64 | |
Frédéric Péters | e0bdd1c60f | |
Frédéric Péters | 23ab03be8a | |
Frédéric Péters | a97b93d163 | |
Frédéric Péters | 71f4a0fd3e | |
Frédéric Péters | 5395f9255f | |
Frédéric Péters | af22690d31 | |
Frédéric Péters | e4f335a604 | |
Frédéric Péters | f5880908bd | |
Frédéric Péters | 0f5d4555d2 | |
Frédéric Péters | f8587a00e1 | |
Frédéric Péters | 9cb2e4cc43 | |
Frédéric Péters | 8831c91a51 | |
Frédéric Péters | 697949b551 | |
Frédéric Péters | 8f3419baf1 | |
Frédéric Péters | 0e120fb4d8 | |
Frédéric Péters | 654d3d3b91 | |
Frédéric Péters | e43bba68f5 | |
Frédéric Péters | dbbb116552 | |
Frédéric Péters | ec09c2ea6a | |
Frédéric Péters | 7071ec96c4 | |
Frédéric Péters | 1bdd4cfda2 | |
Frédéric Péters | 0e01b626b1 | |
Frédéric Péters | 0a68468493 | |
Frédéric Péters | d0b4f24584 | |
Frédéric Péters | ba6e5cd5a1 | |
Frédéric Péters | 2aed9c1bc4 | |
Frédéric Péters | 344498ae4d | |
Frédéric Péters | 228457be9d | |
Frédéric Péters | 4e6f21ef43 | |
Frédéric Péters | 5a4302b461 | |
Frédéric Péters | 4b181fc1bf | |
Frédéric Péters | fd0b882578 | |
Frédéric Péters | e9f5112fbe | |
Frédéric Péters | a231aa0d97 | |
Frédéric Péters | bed2079a3e | |
Frédéric Péters | 259c62a197 | |
Frédéric Péters | 82cc9a3a98 | |
Frédéric Péters | 9fe1e84921 | |
Frédéric Péters | 8be31ef846 | |
Frédéric Péters | 53812ecd8e | |
Frédéric Péters | 7bd1967fc4 | |
Frédéric Péters | 22ba97571f | |
Frédéric Péters | ec72e371d3 | |
Frédéric Péters | c3fe5545ac | |
Frédéric Péters | 30ff9ff561 | |
Frédéric Péters | e541759a67 | |
Frédéric Péters | 1987c5407f | |
Frédéric Péters | fd88b46128 | |
Frédéric Péters | 5da68239d3 | |
Frédéric Péters | db6fe5f504 | |
Frédéric Péters | 4746d75099 | |
Frédéric Péters | 05abc1038d | |
Frédéric Péters | a295a0b441 | |
Frédéric Péters | e4333e6719 | |
Frédéric Péters | 6d8c2ffb14 | |
Frédéric Péters | 6f279b7b28 | |
Frédéric Péters | ef77a23f79 | |
Frédéric Péters | 7d7cea05a6 | |
Frédéric Péters | b79a1f5fbc | |
Frédéric Péters | a6e27b1df3 | |
Frédéric Péters | cf248b9e5a | |
Frédéric Péters | 5f1629a898 | |
Frédéric Péters | 90fdf2c398 | |
Frédéric Péters | 0f4fbdd7ff | |
Frédéric Péters | 1a7e2ec81b | |
Frédéric Péters | d174de9cf7 | |
Frédéric Péters | 95c5c36103 | |
Frédéric Péters | 07357b8549 | |
Frédéric Péters | b73c5cc666 | |
Frédéric Péters | dbec13c73d | |
Frédéric Péters | b46c654323 | |
Frédéric Péters | 8c0658c2c3 | |
Frédéric Péters | d4e9a16468 | |
Frédéric Péters | 56e9be1142 | |
Frédéric Péters | 6d80317832 | |
Frédéric Péters | 4fced111d6 | |
Frédéric Péters | 1346c90386 | |
Frédéric Péters | d4840892b4 | |
Frédéric Péters | d6bce19689 | |
Frédéric Péters | 9dd19f2f05 | |
Frédéric Péters | 4f827110bf | |
Frédéric Péters | 0db7cb0804 | |
Frédéric Péters | 61efee3db9 | |
Frédéric Péters | be4101e5a7 | |
Frédéric Péters | 5d7bc70c0f | |
Frédéric Péters | a4743c216c | |
Frédéric Péters | 11ad87508c | |
Frédéric Péters | 2927c87edd | |
Frédéric Péters | 0e968f3b83 | |
Frédéric Péters | a321f200be | |
Frédéric Péters | 4d39395c39 | |
Frédéric Péters | 48ffcf975a | |
Frédéric Péters | 1ea19f1c74 | |
Frédéric Péters | e4bd408044 | |
Frédéric Péters | d33ae185a7 | |
Frédéric Péters | 75516f7b6a | |
Frédéric Péters | 78c4a5f640 | |
Frédéric Péters | c586e8262d | |
Frédéric Péters | 87253343d4 | |
Frédéric Péters | a17d189124 | |
Frédéric Péters | 29d0f9e8eb | |
Frédéric Péters | 86a623669a | |
Frédéric Péters | 71508015a3 | |
Frédéric Péters | 650c9c3093 | |
Frédéric Péters | 7614f40bcd | |
Frédéric Péters | b9c1293608 | |
Frédéric Péters | deaf0c34f1 | |
Frédéric Péters | 34a382f03e | |
Frédéric Péters | 95bc775346 |
|
@ -1,9 +1,5 @@
|
|||
[run]
|
||||
omit = wcs/qommon/vendor/*.py
|
||||
dynamic_context = test_function
|
||||
|
||||
[report]
|
||||
omit = wcs/qommon/vendor/*.py
|
||||
|
||||
[html]
|
||||
show_contexts = True
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
# trivial: apply black
|
||||
4ebe82ef21fed8353ac38f8257c69a1a31322634
|
||||
# trivial: reapply black
|
||||
a11be8fa590ad3d02a903dacb8393336b989a1a6
|
||||
# trivial: apply new isort configuration (#52504)
|
||||
08f1431a665aec6586960e78cbfda6da47aa6862
|
||||
# misc: apply isort (#52224)
|
||||
48470c50c0e9b56e989cfbcdcec433de0cc8479a
|
||||
# misc: apply pyupgrade (#55490)
|
||||
ff0d3779c024ba3a0109b91d9337aadd06b06788
|
||||
# misc: apply black 22.1.0
|
||||
877155f01d014e8fc778014c55e6a693247261f7
|
||||
# misc: apply djhtml (#69419)
|
||||
dfdbaf2b8ab7202643701eb87edbdee1b1a137e4
|
||||
# misc: apply django-upgrade (#69799)
|
||||
77ad58bf8f16303d19d9f16352bb6ff8ca6d0e98
|
||||
# misc: apply double-quote-string-fixer (#80309)
|
||||
1e2264dd8c0557353f14e6d38e8b29389cd9bce4
|
|
@ -1,16 +1,2 @@
|
|||
**/*.css.map
|
||||
**/django.mo
|
||||
*.pyc
|
||||
.coverage
|
||||
/wcs/qommon/static/css/dc2/admin.css
|
||||
/wcs/qommon/static/css/qommon.css
|
||||
/wcs/qommon/static/css/item-with-image.css
|
||||
MANIFEST
|
||||
build/
|
||||
coverage.xml
|
||||
data/themes/publik-base
|
||||
htmlcov/
|
||||
junit-*.xml
|
||||
local_settings.py
|
||||
pylint.out
|
||||
wcs.egg-info/
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: double-quote-string-fixer
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: ['--keep-percent-format', '--py39-plus']
|
||||
- repo: https://github.com/adamchainz/django-upgrade
|
||||
rev: 1.10.0
|
||||
hooks:
|
||||
- id: django-upgrade
|
||||
args: ['--target-version', '3.2']
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ['--target-version', 'py39', '--skip-string-normalization', '--line-length', '110']
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ['--profile', 'black', '--line-length', '110']
|
||||
- repo: https://github.com/rtts/djhtml
|
||||
rev: '3.0.5'
|
||||
hooks:
|
||||
- id: djhtml
|
||||
args: ['--tabwidth', '2']
|
||||
- repo: https://git.entrouvert.org/pre-commit-debian.git
|
||||
rev: v0.3
|
||||
hooks:
|
||||
- id: pre-commit-debian
|
|
@ -1,12 +1,18 @@
|
|||
@Library('eo-jenkins-lib@main') import eo.Utils
|
||||
@Library('eo-jenkins-lib@master') import eo.Utils
|
||||
|
||||
pipeline {
|
||||
agent any
|
||||
options { disableConcurrentBuilds() }
|
||||
stages {
|
||||
stage('Unit Tests (Python 2)') {
|
||||
steps {
|
||||
sh 'tox -r -e py2'
|
||||
}
|
||||
}
|
||||
stage('Unit Tests') {
|
||||
steps {
|
||||
sh 'NUMPROCESSES=8 tox -rv'
|
||||
sh 'git clean -xdf'
|
||||
sh 'tox -r -e py3-pylint-coverage'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
|
@ -16,26 +22,17 @@ pipeline {
|
|||
utils.publish_coverage_native('index.html')
|
||||
utils.publish_pylint('pylint.out')
|
||||
}
|
||||
mergeJunitResults()
|
||||
junit '*_results.xml'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Packaging') {
|
||||
agent any
|
||||
steps {
|
||||
script {
|
||||
env.SHORT_JOB_NAME=sh(
|
||||
returnStdout: true,
|
||||
// given JOB_NAME=gitea/project/PR-46, returns project
|
||||
// given JOB_NAME=project/main, returns project
|
||||
script: '''
|
||||
echo "${JOB_NAME}" | sed "s/gitea\\///" | awk -F/ '{print $1}'
|
||||
'''
|
||||
).trim()
|
||||
if (env.GIT_BRANCH == 'main' || env.GIT_BRANCH == 'origin/main') {
|
||||
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d bullseye,bookworm ${SHORT_JOB_NAME}"
|
||||
if (env.JOB_NAME == 'wcs' && env.GIT_BRANCH == 'origin/master') {
|
||||
sh 'sudo -H -u eobuilder /usr/local/bin/eobuilder wcs'
|
||||
} else if (env.GIT_BRANCH.startsWith('hotfix/')) {
|
||||
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d bullseye,bookworm --branch ${env.GIT_BRANCH} --hotfix ${SHORT_JOB_NAME}"
|
||||
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder --branch ${env.GIT_BRANCH} --hotfix wcs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +42,7 @@ pipeline {
|
|||
always {
|
||||
script {
|
||||
utils = new Utils()
|
||||
utils.mail_notify(currentBuild, env, 'ci+jenkins-wcs@entrouvert.org')
|
||||
utils.mail_notify(currentBuild, env, 'fpeters+jenkins-wcs@entrouvert.org')
|
||||
}
|
||||
}
|
||||
success {
|
||||
|
|
|
@ -4,7 +4,9 @@ include wcs.cfg-sample
|
|||
recursive-include wcs/locale *.po *.mo
|
||||
recursive-include extra/ *.py
|
||||
recursive-include data/web/ *.html *.css *.png
|
||||
recursive-include data/themes/default/ *.html *.css *.png *.gif *.jpg *.js *.ezt *.xml
|
||||
recursive-include data/themes/alto/ *.html *.css *.png *.gif *.jpg *.js *.ezt *.xml
|
||||
recursive-include data/vendor/ *.dat
|
||||
recursive-include wcs/qommon/static/ *.css *.scss *.png *.gif *.jpg *.js *.eot *.svg *.ttf *.woff *.map
|
||||
recursive-include wcs/qommon/static/ *.css *.png *.gif *.jpg *.js *.eot *.svg *.ttf *.woff
|
||||
recursive-include wcs/templates *.html *.txt
|
||||
recursive-include wcs/qommon/templates *.html *.txt
|
||||
|
|
50
README
|
@ -31,34 +31,6 @@ Then you need to run the tests
|
|||
It is possible to pass a --without-postgresql-tests parameter to skip the
|
||||
PostgreSQL tests.
|
||||
|
||||
|
||||
Code Style
|
||||
----------
|
||||
|
||||
black is used to format the code, using thoses parameters:
|
||||
|
||||
black --target-version py37 --skip-string-normalization --line-length 110
|
||||
|
||||
isort is used to format the imports, using those parameters:
|
||||
|
||||
isort --profile black --line-length 110
|
||||
|
||||
pyupgrade is used to automatically upgrade syntax, using those parameters:
|
||||
|
||||
pyupgrade --keep-percent-format --py37-plus
|
||||
|
||||
djhtml is used to automatically indent html files, using those parameters:
|
||||
|
||||
djhtml --tabwidth 2
|
||||
|
||||
django-upgrade is used to automatically upgrade Django syntax, using those parameters:
|
||||
|
||||
django-upgrade --target-version 2.2
|
||||
|
||||
There is .pre-commit-config.yaml to use pre-commit to automatically run these tools
|
||||
before commits. (execute `pre-commit install` to install the git hook.)
|
||||
|
||||
|
||||
Copyright
|
||||
---------
|
||||
|
||||
|
@ -159,6 +131,14 @@ jQuery JavaScript Library:
|
|||
# Dual licensed under the MIT and GPL licenses.
|
||||
# http://docs.jquery.com/License
|
||||
|
||||
jQuery kiketable.colsizable plugin:
|
||||
# Copyright (c) 2007-2009 Enrique Meléndez Estrada
|
||||
# Dual licensed under the MIT and GPL licenses:
|
||||
|
||||
Tabs - jQuery plugin for accessible, unobtrusive tabs:
|
||||
# Copyright (c) 2006 Klaus Hartl (stilbuero.de)
|
||||
# Dual licensed under the MIT and GPL licenses:
|
||||
|
||||
TableSorter 2.0 - Client-side table sorting with ease!:
|
||||
# Copyright (c) 2007 Christian Bach
|
||||
# Dual licensed under the MIT and GPL licenses:
|
||||
|
@ -175,6 +155,20 @@ WYSIWYG - jQuery plugin 0.3
|
|||
#
|
||||
# Dual licensed under the MIT and GPL licenses:
|
||||
|
||||
Treeview 1.4 - jQuery plugin to hide and show branches of a tree
|
||||
# Copyright (c) 2007 Jörn Zaefferer
|
||||
#
|
||||
# Dual licensed under the MIT and GPL licenses:
|
||||
|
||||
jQuery Date Picker:
|
||||
# Copyright (c) 2007 Kelvin Luck (http://www.kelvinluck.com/)
|
||||
# Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
|
||||
# and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
|
||||
|
||||
bgiframe:
|
||||
# Copyright (c) 2010 Brandon Aaron (http://brandonaaron.net)
|
||||
# Licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php)
|
||||
|
||||
svg-pan-zoom:
|
||||
# Copyright 2009-2010 Andrea Leofreddi <a.leofreddi@itcharm.com>
|
||||
# Licensed under the BSD 2-clause license (http://opensource.org/licenses/BSD-2-Clause)
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0"?>
|
||||
<theme name="alto" version="1.0">
|
||||
<label>Alto</label>
|
||||
<desc>Alto theme</desc>
|
||||
<author>Frederic Peters (original Dotclear theme (alto studio) by David Jubert)</author>
|
||||
</theme>
|
After Width: | Height: | Size: 728 B |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 508 B |
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="[site_lang]">
|
||||
<head>
|
||||
<title>[page_title]</title>
|
||||
<link rel="stylesheet" type="text/css" href="[css]"/>
|
||||
[script]
|
||||
</head>
|
||||
<body[if-any onload] onload="[onload]"[end]>
|
||||
<div id="page">
|
||||
<div id="top"> <h1>[if-any title][title][else][site_name][end]</h1> </div>
|
||||
<div id="main-content">
|
||||
[if-any breadcrumb]<p id="breadcrumb">Vous êtes ici : [breadcrumb]</p>[end]
|
||||
[body]
|
||||
</div>
|
||||
<div id="footer"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,270 @@
|
|||
/* adapted from alto dotclear theme */
|
||||
|
||||
@import url(/static/xstatic/themes/smoothness/jquery-ui.min.css);
|
||||
@import url(/static/css/qommon.css);
|
||||
|
||||
html, body {
|
||||
background: #CCCCCC;
|
||||
font-family: sans-serif;
|
||||
color: #333333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: none;
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
cursor: hand;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
input,textarea {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 99%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
text-decoration : none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0273B9;
|
||||
text-decoration : underline;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #0273B9;
|
||||
text-decoration : none;
|
||||
}
|
||||
|
||||
#page {
|
||||
background: #fff url(img/page.jpg) repeat-y center top;
|
||||
color: inherit;
|
||||
width: 886px;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#top {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #CCCCCC url(img/top.jpg) no-repeat left top;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
#top h1 {
|
||||
width: 706px;
|
||||
margin: 0 auto;
|
||||
padding-top: 70px;
|
||||
}
|
||||
|
||||
#side {
|
||||
float: right;
|
||||
width: 204px;
|
||||
padding: 0;
|
||||
margin: 0 -20px 0 20px;
|
||||
}
|
||||
|
||||
#side #tracking-code {
|
||||
margin-bottom: 1em;
|
||||
border: 1px solid #bfbfbf;
|
||||
color: #333333;
|
||||
background: #e6e6e6;
|
||||
padding: 1ex;
|
||||
}
|
||||
|
||||
#side #tracking-code h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#side #tracking-code button,
|
||||
#side #tracking-code a {
|
||||
margin: 1ex auto;
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 120%;
|
||||
background: white;
|
||||
border: 1px solid black;
|
||||
padding: 0.5ex 0;
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
#side #tracking-code button {
|
||||
background: #0273B9;
|
||||
color: white;
|
||||
}
|
||||
|
||||
input[name=savedraft] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#steps {
|
||||
background: white;
|
||||
border: 1px solid #bfbfbf;
|
||||
color: #333333;
|
||||
background: #e6e6e6;
|
||||
-moz-border-radius: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
#footer {
|
||||
width: 886px;
|
||||
height: 123px;
|
||||
background: #CCCCCC url(img/bottom.jpg) no-repeat left top;
|
||||
margin: 0;
|
||||
margin-top: 1em;
|
||||
color: #666;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#footer p {
|
||||
width: 706px;
|
||||
margin: 0 auto;
|
||||
padding-top: 24px;
|
||||
text-align: right;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
|
||||
#main-content {
|
||||
width: 735px;
|
||||
padding-left: 65px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
|
||||
div#steps ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
div#steps li {
|
||||
display: block;
|
||||
border: 1px solid #ddd;
|
||||
margin: 0.5em 0;
|
||||
background: #eee;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
#steps span.marker {
|
||||
padding: 0 1ex 0 1ex;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
text-align: center;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
#steps li.current span.marker {
|
||||
background: #0273b9;
|
||||
}
|
||||
|
||||
|
||||
#steps li.current {
|
||||
font-weight: bold;
|
||||
border: 1px solid #333333;
|
||||
}
|
||||
|
||||
#steps li.current span.label {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
#steps ol ul {
|
||||
margin-right: 1em;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
#steps ol ul li {
|
||||
padding: 0 2px;
|
||||
font-weight: normal;
|
||||
margin-left: -1ex;
|
||||
}
|
||||
|
||||
#steps ol ul li.current {
|
||||
border-color: inherit;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
|
||||
div.widget {
|
||||
clear: none;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
hr {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
textarea {
|
||||
}
|
||||
|
||||
p#breadcrumb {
|
||||
background: #e6e6e6;
|
||||
-moz-border-radius: 6px;
|
||||
width: 750px;
|
||||
padding: 3px;
|
||||
font-size: 90%;
|
||||
border: 1px solid #bfbfbf;
|
||||
}
|
||||
|
||||
div#receipt {
|
||||
}
|
||||
|
||||
div#receipt span.label {
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
div#receipt span.value {
|
||||
display: block;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
form div.page,
|
||||
div#receipt div.page {
|
||||
border: 1px solid #bfbfbf;
|
||||
padding: 1ex;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
form div.page p,
|
||||
div#receipt div.page p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
form div.page h3,
|
||||
div#receipt div.page h3 {
|
||||
margin: 0;
|
||||
margin-bottom: 1ex;
|
||||
}
|
||||
|
||||
|
||||
p#receiver {
|
||||
margin: 0;
|
||||
margin-left: 2em;
|
||||
margin-top: -0.7em;
|
||||
margin-bottom: 1em;
|
||||
padding: 2px 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
table#listing {
|
||||
background: white;
|
||||
border: 1px solid #888;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0"?>
|
||||
<theme name="default" version="1.0">
|
||||
<label>Default</label>
|
||||
<desc>Default theme</desc>
|
||||
<author>Frederic Peters & Dotclear Team</author>
|
||||
</theme>
|
After Width: | Height: | Size: 675 B |
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>[page_title]</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, user-scalable=0">
|
||||
<script type="text/javascript" src="[root_url]static/xstatic/jquery.js"></script>
|
||||
[script]
|
||||
<script type="text/javascript" src="[root_url]static/js/wcs.mobile.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="[root_url]static/css/mobile.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="[theme_url]/mobile.css"/>
|
||||
</head>
|
||||
<body[if-any onload] onload="[onload]"[end]>
|
||||
|
||||
<div id="page">
|
||||
<div id="header">
|
||||
<div id="top">
|
||||
[if-any auquotidien]
|
||||
<a id="menu"><img src="[root_url]qo/images/mobile/menu.png" alt="-"/></a>
|
||||
[end]
|
||||
<h1><a href="/">[site_name]</a></h1>
|
||||
<a id="gear"><img src="[root_url]qo/images/mobile/gear.png" alt="."/></a>
|
||||
</div>
|
||||
</div> <!-- header -->
|
||||
[if-any links]
|
||||
<div id="nav-site" style="display: none;">
|
||||
[links]
|
||||
</div>
|
||||
[end]
|
||||
<div id="nav-user" style="display: none;">
|
||||
<ul>
|
||||
[if-any user]
|
||||
<li><a href="[root_url]logout">Déconnexion</a></li>
|
||||
[else]
|
||||
<li><a href="[root_url]login/">Connexion</a></li>
|
||||
<li><a href="[root_url]register/">Inscription</a></li>
|
||||
[end]
|
||||
[if-any auquotidien]
|
||||
<li><a href="[root_url]informations-editeur">Mentions légales</a></li>
|
||||
[end]
|
||||
<li><a href="?toggle-mobile">Affichage classique</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="main-content">
|
||||
|
||||
<div id="content">
|
||||
[if-any title]<h2>[title]</h2>[end]
|
||||
[body]
|
||||
</div> <!-- #content -->
|
||||
|
||||
<br class="clear"/>
|
||||
|
||||
</div> <!-- #main-content -->
|
||||
|
||||
<div id="footer-wrapper">
|
||||
<div id="footer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ site_lang }}">
|
||||
<head>
|
||||
<title>{% block page-title %}{{ page_title }}{% endblock %}</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ css }}"/>
|
||||
{{ script|safe }}
|
||||
{% block extrascripts %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div {% if onload %}onload="{{ onload }}"{% endif %}>
|
||||
<div id="page">
|
||||
<div id="top">
|
||||
{% block header %}
|
||||
<h1>WIP/DJANGO - {% if title %}{{ title }}{% else %}{{ site_name }}{% endif %}</h1>
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div id="main-content">
|
||||
{% block content %}
|
||||
{{ prelude }}
|
||||
|
||||
{% if breadcrumb %}
|
||||
<p id="breadcrumb">{{ breadcrumb|safe }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% block body %}
|
||||
{{ body|safe }}
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div id="footer">{{ footer }}</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,20 @@
|
|||
{% extends "base.html"%}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div>
|
||||
<h2>HELLO WORLD</h2>
|
||||
|
||||
{% regroup forms by category as category_list %}
|
||||
{% for category in category_list %}
|
||||
{% if category.grouper %}<h3>{{ category.grouper }}</h3>{% endif %}
|
||||
<ul>
|
||||
{% for form in category.list %}
|
||||
<li><a href="{{ form.url }}">{{ form.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,12 @@
|
|||
@import url(../../qo/css/sofresh.css);
|
||||
|
||||
#page {
|
||||
-webkit-transform: rotate(2deg);
|
||||
-webkit-transition: all 200ms ease-out;
|
||||
-webkit-filter: grayscale(100%);
|
||||
}
|
||||
|
||||
#page:hover {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-webkit-filter: none;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
@import url(sofresh.css);
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
AbachoBOT
|
||||
abcdatos_botlink
|
||||
http://www.abcdatos.com/botlink/
|
||||
AESOP_com_SpiderMan
|
||||
ah-ha.com crawler (crawler@ah-ha.com)
|
||||
ia_archiver
|
||||
Scooter
|
||||
Mercator
|
||||
Scooter2_Mercator_3-1.0
|
||||
roach.smo.av.com-1.0
|
||||
Tv<nn>_Merc_resh_26_1_D-1.0
|
||||
AltaVista-Intranet
|
||||
jan.gelin@av.com
|
||||
FAST-WebCrawler
|
||||
crawler@fast.no
|
||||
Wget
|
||||
Acoon Robot
|
||||
antibot
|
||||
Atomz
|
||||
AxmoRobot
|
||||
Buscaplus Robi
|
||||
http://www.buscaplus.com/robi/
|
||||
CanSeek/
|
||||
support@canseek.ca
|
||||
ChristCRAWLER
|
||||
http://www.christcrawler.com/
|
||||
Clushbot
|
||||
http://www.clush.com/bot.html
|
||||
Crawler
|
||||
admin@crawler.de
|
||||
DaAdLe.com ROBOT/
|
||||
RaBot
|
||||
Agent-admin/ phortse@hanmail.net
|
||||
contact/jylee@kies.co.kr
|
||||
RaBot
|
||||
Agent-admin/ webmaster@kisco.go.kr
|
||||
DeepIndex
|
||||
DittoSpyder
|
||||
Jack
|
||||
EARTHCOM.info
|
||||
Speedy Spider
|
||||
ArchitextSpider
|
||||
ArchitectSpider
|
||||
EuripBot
|
||||
Arachnoidea
|
||||
arachnoidea@euroseek.net
|
||||
EZResult
|
||||
Fast PartnerSite Crawler
|
||||
FAST Data Search Crawler
|
||||
FAST Data Search Document Retriever
|
||||
KIT-Fireball
|
||||
france.misesajour.com
|
||||
FyberSearch
|
||||
GalaxyBot
|
||||
http://www.galaxy.com/galaxybot.html
|
||||
geckobot
|
||||
GenCrawler
|
||||
GeonaBot
|
||||
getRAX
|
||||
Googlebot
|
||||
googlebot@googlebot.com
|
||||
http://googlebot.com/
|
||||
moget/2.0
|
||||
moget@goo.ne.jp
|
||||
Aranha
|
||||
Slurp.so/1.0
|
||||
slurp@inktomi.com
|
||||
Slurp/2.0j
|
||||
slurp@inktomi.com
|
||||
www.inktomisearch.com
|
||||
Slurp/2.0-KiteHourly
|
||||
slurp@inktomi.com;
|
||||
www.inktomi.com/slurp.html
|
||||
Slurp/2.0-OwlWeekly
|
||||
spider@aeneid.com
|
||||
www.inktomi.com/slurp.html
|
||||
Slurp/3.0-AU
|
||||
slurp@inktomi.com
|
||||
Toutatis 2.5-2
|
||||
Hubater
|
||||
http://www.almaden.ibm.com/cs/crawler
|
||||
IlTrovatore-Setaccio
|
||||
IncyWincy
|
||||
UltraSeek
|
||||
InfoSeek Sidewinder
|
||||
Mole2/1.0
|
||||
webmaster@intags.de
|
||||
MP3Bot
|
||||
C-PBWF-ip3000.com-crawler
|
||||
ip3000.com-crawler
|
||||
http://www.istarthere.com
|
||||
spider@istarthere.com
|
||||
Knowledge.com/
|
||||
kuloko-bot/0.2
|
||||
LNSpiderguy
|
||||
Linknzbot
|
||||
lookbot
|
||||
MantraAgent
|
||||
NetResearchServer
|
||||
www.loopimprovements.com/robot.html
|
||||
Lycos_Spider_(T-Rex)
|
||||
JoocerBot
|
||||
HenryTheMiragoRobot
|
||||
MojeekBot
|
||||
mozDex/
|
||||
MSNBOT/0.1
|
||||
http://search.msn.com/msnbot.htm)
|
||||
Navadoo Crawler
|
||||
Gulliver
|
||||
ObjectsSearch/0.01
|
||||
PicoSearch/
|
||||
PJspider
|
||||
DIIbot
|
||||
nttdirectory_robot
|
||||
super-robot@super.navi.ocn.ne.jp
|
||||
griffon
|
||||
griffon@super.navi.ocn.ne.jp
|
||||
Spider/maxbot.com
|
||||
admin@maxbot.com
|
||||
various (fakes agent on each access)
|
||||
gazz/1.0
|
||||
gazz@nttrd.com
|
||||
???
|
||||
NationalDirectory-SuperSpider
|
||||
dloader(NaverRobot)/
|
||||
dumrobo(NaverRobot)/
|
||||
Openfind piranha,Shark
|
||||
robot-response@openfind.com.tw
|
||||
Openbot/
|
||||
psbot
|
||||
www.picsearch.org/bot.html
|
||||
CrawlerBoy Pinpoint.com
|
||||
user<n>.ip3000.com
|
||||
QweeryBot
|
||||
http://qweerybot.qweery.com)
|
||||
AlkalineBOT
|
||||
SeznamBot
|
||||
Search-10
|
||||
Fluffy the spider
|
||||
info@searchhippo.com)
|
||||
Scrubby/
|
||||
asterias
|
||||
speedfind ramBot xtreme
|
||||
Kototoi/0.1
|
||||
SearchByUsa
|
||||
Searchspider/
|
||||
SightQuestBot/
|
||||
http://www.sightquest.com/bot.htm
|
||||
Spider_Monkey/
|
||||
Surfnomore Spider v1.1
|
||||
Robot@SuperSnooper.Com
|
||||
teoma_agent1
|
||||
teoma_admin@hawkholdings.com
|
||||
Teradex_Mapper
|
||||
mapper@teradex.com
|
||||
ESISmartSpider
|
||||
Spider TraficDublu
|
||||
Tutorial Crawler
|
||||
http://www.tutorgig.com/crawler
|
||||
updated/0.1beta
|
||||
crawler@updated.com
|
||||
UK Searcher Spider
|
||||
Vivante Link Checker
|
||||
appie
|
||||
Nazilla
|
||||
www.WebWombat.com.au
|
||||
marvin/infoseek
|
||||
marvin-team@webseek.de
|
||||
MuscatFerret
|
||||
WhizBang! Lab
|
||||
ZyBorg
|
||||
(info@WISEnut.com)
|
||||
WIRE WebRefiner:
|
||||
webrefiner@wire.co.uk
|
||||
WSCbot
|
||||
Yandex
|
||||
Yellopet-Spider
|
||||
libwww-perl
|
||||
Iron33
|
|
@ -0,0 +1 @@
|
|||
9
|
|
@ -2,60 +2,40 @@ Source: wcs
|
|||
Section: web
|
||||
Priority: optional
|
||||
Maintainer: Frederic Peters <fpeters@debian.org>
|
||||
Build-Depends: debhelper-compat (= 12),
|
||||
dh-python,
|
||||
gettext,
|
||||
python3-all,
|
||||
python3-gadjo,
|
||||
python3-quixote,
|
||||
python3-setuptools,
|
||||
sassc,
|
||||
Build-Depends: python-quixote, debhelper (>= 9), dh-python, dh-systemd, python-setuptools, gettext, python-gadjo
|
||||
Standards-Version: 3.9.6.0
|
||||
Homepage: https://dev.entrouvert.org/projects/wcs/
|
||||
X-Python-Version: 2.7
|
||||
|
||||
Package: wcs
|
||||
Architecture: all
|
||||
Depends: graphviz,
|
||||
python3-bleach,
|
||||
python3-distutils,
|
||||
python3-django (>= 2:3.2),
|
||||
python3-django-ckeditor,
|
||||
python3-django-ratelimit,
|
||||
python3-dnspython,
|
||||
python3-emoji,
|
||||
python3-freezegun,
|
||||
python3-hobo,
|
||||
python3-lasso,
|
||||
python3-lxml,
|
||||
python3-pil,
|
||||
python3-psutil,
|
||||
python3-psycopg2,
|
||||
python3-pyproj,
|
||||
python3-quixote,
|
||||
python3-requests,
|
||||
python3-setproctitle,
|
||||
python3-unidecode,
|
||||
python3-uwsgidecorators,
|
||||
python3-vobject,
|
||||
python3-xstatic-godo,
|
||||
python3-xstatic-leaflet,
|
||||
python3-xstatic-leaflet-gesturehandling,
|
||||
python3-xstatic-select2,
|
||||
uwsgi,
|
||||
uwsgi-plugin-python3,
|
||||
${misc:Depends},
|
||||
${python3:Depends},
|
||||
Recommends: graphicsmagick,
|
||||
libreoffice-writer-nogui | libreoffice-writer,
|
||||
poppler-utils,
|
||||
python3-docutils,
|
||||
python3-langdetect,
|
||||
python3-magic,
|
||||
python3-qrcode,
|
||||
python3-workalendar,
|
||||
Suggests: python3-libxml2,
|
||||
Depends: ${misc:Depends}, ${python:Depends},
|
||||
python-django (>= 1.8),
|
||||
python-quixote,
|
||||
python-hobo,
|
||||
graphviz,
|
||||
python-django-ckeditor,
|
||||
python-django-ratelimit,
|
||||
python-feedparser,
|
||||
python-imaging,
|
||||
python-pyproj,
|
||||
python-requests,
|
||||
python-vobject,
|
||||
python-xstatic-leaflet,
|
||||
uwsgi,
|
||||
uwsgi-plugin-python
|
||||
Recommends: python-dns,
|
||||
python-xlwt,
|
||||
python-qrcode,
|
||||
python-magic,
|
||||
python-docutils,
|
||||
poppler-utils
|
||||
Suggests: python-libxml2,
|
||||
python-lasso,
|
||||
python-psycopg2
|
||||
Description: web application to design and set up online forms
|
||||
w.c.s. is a web application which allows to design and set up online forms.
|
||||
.
|
||||
It gives a user the ability to create web forms easily without requiring
|
||||
any other skill than familiarity with web surfing
|
||||
|
||||
|
|
|
@ -24,3 +24,4 @@ Place - Suite 330, Boston, MA 02111-1307, USA.
|
|||
|
||||
On Debian GNU/Linux systems, the complete text of the GNU General Public
|
||||
License can be found in `/usr/share/common-licenses/GPL'.
|
||||
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
# This file is sourced by "exec(open(..." from wcs.settings
|
||||
# This file is sourced by "execfile" from wcs.settings
|
||||
|
||||
import os
|
||||
|
||||
PROJECT_NAME = 'wcs'
|
||||
WCS_MANAGE_COMMAND = '/usr/bin/wcs-manage'
|
||||
|
||||
#
|
||||
# hobotization
|
||||
#
|
||||
exec(open('/usr/lib/hobo/debian_config_common.py').read())
|
||||
execfile('/usr/lib/hobo/debian_config_common.py')
|
||||
|
||||
# and some hobo parts that are specific to w.c.s.
|
||||
TEMPLATES[0]['OPTIONS']['context_processors'] = [
|
||||
'hobo.context_processors.template_vars',
|
||||
'hobo.context_processors.theme_base',
|
||||
'hobo.context_processors.user_urls',
|
||||
] + TEMPLATES[0]['OPTIONS']['context_processors']
|
||||
'hobo.context_processors.template_vars',
|
||||
'hobo.context_processors.theme_base',
|
||||
] + TEMPLATES[0]['OPTIONS']['context_processors']
|
||||
|
||||
MIDDLEWARE = (
|
||||
if not 'MIDDLEWARE_CLASSES' in globals():
|
||||
MIDDLEWARE_CLASSES = global_settings.MIDDLEWARE_CLASSES
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'hobo.middleware.utils.StoreRequestMiddleware',
|
||||
'hobo.middleware.xforwardedfor.XForwardedForMiddleware',
|
||||
'hobo.middleware.VersionMiddleware', # /__version__
|
||||
'hobo.middleware.VersionMiddleware', # /__version__
|
||||
'hobo.middleware.cors.CORSMiddleware',
|
||||
) + MIDDLEWARE
|
||||
) + MIDDLEWARE_CLASSES
|
||||
|
||||
CACHES = {
|
||||
'default': {
|
||||
|
@ -35,12 +36,11 @@ CACHES = {
|
|||
|
||||
# don't rely on hobo logging as it requires hobo multitenant support.
|
||||
LOGGING = {}
|
||||
LOGGING_CONFIG = None
|
||||
|
||||
#
|
||||
# local settings
|
||||
#
|
||||
exec(open(os.path.join(ETC_DIR, 'settings.py')).read())
|
||||
execfile(os.path.join(ETC_DIR, 'settings.py'))
|
||||
|
||||
# run additional settings snippets
|
||||
exec(open('/usr/lib/hobo/debian_config_settings_d.py').read())
|
||||
execfile('/usr/lib/hobo/debian_config_settings_d.py')
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
#!/usr/bin/make -f
|
||||
# GNU copyright 1997 to 1999 by Joey Hess.
|
||||
|
||||
export PYBUILD_NAME=wcs
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
|
||||
%:
|
||||
dh $@ --with python3 --buildsystem=pybuild
|
||||
dh $@ --with python2,systemd
|
||||
|
||||
override_dh_install:
|
||||
dh_install
|
||||
mv $(CURDIR)/debian/wcs/usr/bin/wcsctl.py \
|
||||
$(CURDIR)/debian/wcs/usr/bin/wcsctl
|
||||
mv $(CURDIR)/debian/wcs/usr/bin/manage.py \
|
||||
$(CURDIR)/debian/wcs/usr/lib/wcs/
|
||||
install -d $(CURDIR)/debian/wcs/etc/wcs
|
||||
install -m 644 wcs.cfg-sample $(CURDIR)/debian/wcs/etc/wcs/wcs.cfg
|
||||
|
||||
override_dh_auto_test:
|
||||
# skip upstream tests
|
||||
|
|
|
@ -14,15 +14,15 @@
|
|||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
|
||||
# ADMINS = (
|
||||
#ADMINS = (
|
||||
# # ('User 1', 'watchdog@example.net'),
|
||||
# # ('User 2', 'janitor@example.net'),
|
||||
# )
|
||||
#)
|
||||
|
||||
# ALLOWED_HOSTS must be correct in production!
|
||||
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
||||
ALLOWED_HOSTS = [
|
||||
'*',
|
||||
'*',
|
||||
]
|
||||
|
||||
# Databases
|
||||
|
@ -54,3 +54,4 @@ TIME_ZONE = 'Europe/Paris'
|
|||
# SESSION_COOKIE_SECURE = True
|
||||
|
||||
WCS_LEGACY_CONFIG_FILE = '/etc/wcs/wcs.cfg'
|
||||
WCS_EXTRA_MODULES = []
|
||||
|
|
|
@ -1,42 +1,18 @@
|
|||
[uwsgi]
|
||||
auto-procname = true
|
||||
procname-prefix-spaced = wcs
|
||||
strict = true
|
||||
|
||||
plugin = python3
|
||||
single-interpreter = true
|
||||
plugin = python
|
||||
module = wcs.wsgi:application
|
||||
need-app = true
|
||||
|
||||
http-socket = /run/wcs/wcs.sock
|
||||
chmod-socket = 666
|
||||
vacuum = true
|
||||
|
||||
spooler-processes = 3
|
||||
spooler-python-import = wcs.qommon.spooler
|
||||
spooler-max-tasks = 20
|
||||
# spooler directory is set using the command line in systemd unit file / init.d startup file.
|
||||
|
||||
master = true
|
||||
enable-threads = true
|
||||
processes = 10
|
||||
harakiri = 120
|
||||
|
||||
processes = 500
|
||||
|
||||
plugin = cheaper_busyness
|
||||
cheaper-algo = busyness
|
||||
cheaper = 5
|
||||
cheaper-initial = 10
|
||||
cheaper-overload = 20
|
||||
cheaper-step = 2
|
||||
cheaper-busyness-multiplier = 10
|
||||
cheaper-busyness-min = 20
|
||||
cheaper-busyness-max = 70
|
||||
cheaper-busyness-backlog-alert = 16
|
||||
cheaper-busyness-backlog-step = 2
|
||||
|
||||
listen = 1024
|
||||
|
||||
enable-threads = true
|
||||
max-requests = 500
|
||||
max-worker-lifetime = 7200
|
||||
|
||||
|
@ -44,10 +20,8 @@ buffer-size = 32768
|
|||
|
||||
py-tracebacker = /run/wcs/py-tracebacker.sock.
|
||||
stats = /run/wcs/stats.sock
|
||||
memory-report = true
|
||||
|
||||
ignore-sigpipe = true
|
||||
disable-write-exception = true
|
||||
|
||||
if-file = /etc/wcs/uwsgi-local.ini
|
||||
include = /etc/wcs/uwsgi-local.ini
|
||||
|
|
|
@ -18,8 +18,8 @@ fi
|
|||
|
||||
if test $# -eq 0
|
||||
then
|
||||
python3 ${MANAGE} help
|
||||
python ${MANAGE} help
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 ${MANAGE} "$@"
|
||||
python ${MANAGE} "$@"
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
MAILTO=root
|
||||
LANG=C.UTF-8
|
||||
|
||||
* * * * * wcs /usr/bin/wcs-manage cron
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
etc/wcs
|
||||
usr/lib/wcs
|
||||
usr/sbin
|
||||
usr/lib/wcs
|
||||
var/lib/wcs
|
||||
var/lib/wcs/collectstatic
|
||||
var/lib/wcs/spooler
|
||||
var/log/wcs
|
||||
|
|
|
@ -17,6 +17,7 @@ set -e
|
|||
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
|
||||
DESC="Web Forms Manager"
|
||||
NAME=wcs
|
||||
WCSCTL=/usr/bin/wcsctl
|
||||
DAEMON=/usr/bin/uwsgi
|
||||
RUN_DIR=/run/$NAME
|
||||
PIDFILE=$RUN_DIR/$NAME.pid
|
||||
|
@ -29,7 +30,6 @@ TIMEOUT=30
|
|||
CONFIG_FILE=/etc/wcs/wcs.cfg
|
||||
WCS_SETTINGS_FILE=/usr/lib/$NAME/debian_config.py
|
||||
MANAGE_SCRIPT="/usr/bin/$NAME-manage"
|
||||
LANG=C.UTF-8
|
||||
|
||||
USER=$NAME
|
||||
GROUP=$NAME
|
||||
|
@ -43,7 +43,6 @@ GROUP=$NAME
|
|||
DAEMON_ARGS=${DAEMON_ARGS:-"--pidfile=$PIDFILE
|
||||
--uid $USER --gid $GROUP
|
||||
--ini /etc/$NAME/uwsgi.ini
|
||||
--spooler /var/lib/wcs/spooler/
|
||||
--daemonize /var/log/uwsgi.$NAME.log"}
|
||||
|
||||
# Load the VERBOSE setting and other rcS variables
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
debian/debian_config.py /usr/lib/wcs
|
||||
debian/wcs-manage /usr/bin
|
||||
debian/settings.py /etc/wcs
|
||||
debian/uwsgi.ini /etc/wcs
|
||||
debian/wcs-manage /usr/bin
|
||||
debian/debian_config.py /usr/lib/wcs
|
||||
|
|
|
@ -3,9 +3,11 @@
|
|||
set -e
|
||||
|
||||
NAME=wcs
|
||||
DAEMON=/usr/bin/wcsctl
|
||||
USER=$NAME
|
||||
GROUP=$NAME
|
||||
CONFIG_DIR=/etc/wcs
|
||||
CONFIG_FILE=/etc/wcs/wcs.cfg
|
||||
MANAGE_SCRIPT="/usr/bin/$NAME-manage"
|
||||
|
||||
# Read config file if it is present.
|
||||
|
@ -14,6 +16,12 @@ then
|
|||
. /etc/default/$NAME
|
||||
fi
|
||||
|
||||
if [ $CONFIG_FILE ]; then
|
||||
COMMAND="$DAEMON -f $CONFIG_FILE"
|
||||
else
|
||||
COMMAND="$DAEMON"
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
|
||||
|
@ -27,7 +35,6 @@ case "$1" in
|
|||
chown $USER:$GROUP /var/log/$NAME
|
||||
chown $USER:$GROUP /var/lib/$NAME
|
||||
chown $USER:$GROUP /var/lib/$NAME/collectstatic
|
||||
chown $USER:$GROUP /var/lib/$NAME/spooler
|
||||
|
||||
# create a secret file
|
||||
SECRET_FILE=$CONFIG_DIR/secret
|
||||
|
@ -41,7 +48,7 @@ case "$1" in
|
|||
;;
|
||||
|
||||
triggered)
|
||||
su -s /bin/sh -c "$MANAGE_SCRIPT hobo_deploy --redeploy" $USER
|
||||
su -s /bin/sh -c "$COMMAND hobo_deploy --redeploy" $USER
|
||||
su -s /bin/sh -c "$MANAGE_SCRIPT collectstatic" $USER
|
||||
exit 0
|
||||
;;
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
[Unit]
|
||||
Description=w.c.s.
|
||||
After=network.target postgresql.service
|
||||
After=network.target syslog.target postgresql.service
|
||||
Wants=postgresql.service
|
||||
|
||||
[Service]
|
||||
SyslogIdentifier=uwsgi/wcs
|
||||
Environment=WCS_SETTINGS_FILE=/usr/lib/%p/debian_config.py
|
||||
Environment=LANG=C.UTF-8
|
||||
User=%p
|
||||
Group=%p
|
||||
ExecStartPre=/usr/bin/wcs-manage migrate
|
||||
ExecStartPre=/usr/bin/wcs-manage collectstatic
|
||||
ExecStartPre=/bin/mkdir -p /var/lib/wcs/spooler/%m/
|
||||
ExecStart=/usr/bin/uwsgi --ini /etc/%p/uwsgi.ini --spooler /var/lib/wcs/spooler/%m/
|
||||
ExecStart=/usr/bin/uwsgi --ini /etc/%p/uwsgi.ini
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
KillSignal=SIGQUIT
|
||||
TimeoutStartSec=0
|
||||
|
@ -20,6 +18,7 @@ PrivateTmp=true
|
|||
Restart=on-failure
|
||||
RuntimeDirectory=wcs
|
||||
Type=notify
|
||||
StandardError=syslog
|
||||
NotifyAccess=all
|
||||
|
||||
[Install]
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
interest-noawait hobo-redeploy
|
||||
interest-noawait /usr/lib/python3/dist-packages/gadjo/static
|
||||
interest-noawait /usr/lib/python2.7/dist-packages/gadjo/static
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
all:
|
||||
$(MAKE) -C fr
|
||||
|
||||
clean:
|
||||
$(MAKE) -C fr clean
|
||||
|
||||
.PHONY: clean
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
REST2HTML = rst2html
|
||||
RST2LATEX = ../scripts/rst2latex.py
|
||||
PDFLATEX = pdflatex
|
||||
|
||||
all: wcs-admin.pdf wcs-admin.html
|
||||
|
||||
%.html: %.rst
|
||||
$(REST2HTML) --stylesheet=default.css --link-stylesheet --language=fr $? > $@
|
||||
|
||||
figures-no-alpha-stamp:
|
||||
-rm -rf figures-no-alpha/
|
||||
mkdir figures-no-alpha/
|
||||
for F in figures/*.png; do \
|
||||
../scripts/removealpha.sh $$F figures-no-alpha/`basename $$F`; \
|
||||
done
|
||||
touch figures-no-alpha-stamp
|
||||
|
||||
%.tex: %.rst figures-no-alpha-stamp
|
||||
cat $? | sed -e 's/figures\//figures-no-alpha\//' \
|
||||
-e 's/ ::$$/ : ::/g' \
|
||||
-e 's/.. section-numbering:://' | $(RST2LATEX) --language=fr > $@
|
||||
|
||||
%.pdf: %.tex custom.tex
|
||||
$(PDFLATEX) $?
|
||||
logfile=`echo "$@" |sed -r "s/(.*)....$$/\\1/"`.log; while [ -f "$$logfile" -a -n "`grep "Rerun to get cross-references right" $$logfile`" ]; do $(PDFLATEX) $< ; done
|
||||
|
||||
clean:
|
||||
-rm *.aux *.toc *.log *.out
|
||||
-rm wcs-admin.pdf
|
||||
-rm wcs-admin.tex
|
||||
-rm wcs-admin.html
|
||||
-rm -rf figures-no-alpha figures-no-alpha-stamp
|
||||
|
||||
.PHONY: all clean
|
|
@ -0,0 +1,45 @@
|
|||
\usepackage{float,fancyhdr,lscape,sectsty,colortbl,color,lastpage,setspace}
|
||||
\usepackage[perpage,bottom]{footmisc}
|
||||
\usepackage[hang]{caption2}
|
||||
\usepackage{marvosym}
|
||||
|
||||
\usepackage{float,url,listings,tocbibind,fancyhdr,calc,placeins}
|
||||
|
||||
\usepackage{palatino}
|
||||
\usepackage[Glenn]{fncychap}
|
||||
|
||||
\pagestyle{fancy}
|
||||
\fancyhead{}
|
||||
\fancyfoot{}
|
||||
\fancyhead[L]{w.c.s.}
|
||||
\fancyhead[R]{Guide de l'administrateur}
|
||||
\fancyfoot[C]{Page \thepage}
|
||||
\addtolength{\headheight}{1.6pt}
|
||||
|
||||
\setlength\parindent{0pt}
|
||||
\setlength{\parskip}{1ex plus 0.5ex minus 0.2ex}
|
||||
\setlength\abovecaptionskip{0.1ex}
|
||||
|
||||
\makeatletter
|
||||
\renewcommand{\maketitle}{\begin{titlepage}%
|
||||
\let\footnotesize\small
|
||||
\let\footnoterule\relax
|
||||
\parindent \z@
|
||||
\reset@font
|
||||
\null\vfil
|
||||
\begin{flushleft}
|
||||
\huge \@title
|
||||
\end{flushleft}
|
||||
\par
|
||||
\hrule height 1pt
|
||||
\par
|
||||
\begin{flushright}
|
||||
\LARGE \@author \par
|
||||
\end{flushright}
|
||||
\vskip 60\p@
|
||||
\vfil\null
|
||||
\end{titlepage}%
|
||||
\setcounter{footnote}{0}%
|
||||
}
|
||||
\makeatother
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
|
||||
h1 a, h2 a, h3 a, h4 a {
|
||||
text-decoration: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
pre.literal-block {
|
||||
background: #eee;
|
||||
border: 1px inset black;
|
||||
padding: 2px;
|
||||
margin: auto 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
h1.title {
|
||||
text-align: center;
|
||||
background: #eef;
|
||||
border: 1px solid #aaf;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
div.section {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
div.section h1 {
|
||||
padding: 0 15px;
|
||||
background: #eef;
|
||||
border: 1px solid #aaf;
|
||||
}
|
||||
|
||||
div.section h2 {
|
||||
padding: 0 15px;
|
||||
background: #eef;
|
||||
border: 1px solid #aaf;
|
||||
}
|
||||
|
||||
div.document {
|
||||
margin-top: 1em;
|
||||
border-top: 1px solid #aaf;
|
||||
border-bottom: 1px solid #aaf;
|
||||
}
|
||||
|
||||
div.section p,
|
||||
div.section ul {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
div.contents {
|
||||
float: right;
|
||||
border: 1px solid black;
|
||||
margin: 1em;
|
||||
background: #eef;
|
||||
max-width: 33%;
|
||||
}
|
||||
|
||||
div#building-liberty-services-with-lasso div#table-of-contents {
|
||||
max-width: inherit;
|
||||
float: none;
|
||||
background: white url(lasso.png) bottom right no-repeat;
|
||||
}
|
||||
|
||||
div.contents ul {
|
||||
padding-left: 1em;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
div.contents li {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
div.contents p {
|
||||
background: #ddf;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid black;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
th.docinfo-name {
|
||||
text-align: right;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 1ex;
|
||||
}
|
||||
|
||||
table.table {
|
||||
margin: 1ex 0;
|
||||
border-spacing: 0px;
|
||||
}
|
||||
|
||||
|
||||
table.table th {
|
||||
padding: 0px 1ex;
|
||||
background: #eef;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
|
||||
table.table td {
|
||||
padding: 0 0.5ex;
|
||||
}
|
||||
|
||||
div.note, div.warning {
|
||||
padding: 0.3ex;
|
||||
padding-left: 60px;
|
||||
min-height: 50px;
|
||||
margin: 1ex 1em;
|
||||
}
|
||||
|
||||
div.note {
|
||||
background: #ffa url(note.png) top left no-repeat;
|
||||
border: 1px solid #fd8;
|
||||
}
|
||||
|
||||
div.warning {
|
||||
background: #ffd url(warning.png) top left no-repeat;
|
||||
}
|
||||
|
||||
p.admonition-title {
|
||||
font-weight: bold;
|
||||
display: inline;
|
||||
display: none;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
div.figure {
|
||||
margin: 0 auto;
|
||||
width: 70%;
|
||||
min-width: 800px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.figure p.caption {
|
||||
font-style: italic;
|
||||
margin: 1ex 0 2em 0;
|
||||
text-align: center;
|
||||
}
|
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 91 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 90 KiB |
|
@ -0,0 +1,490 @@
|
|||
%%% Copyright Ulf A. Lindgren
|
||||
%%%
|
||||
%%% Note Premission is granted to modify this file under
|
||||
%%% the condition that it is saved using another
|
||||
%%% file and package name.
|
||||
%%%
|
||||
%%% Revision 1.1 (1997)
|
||||
%%%
|
||||
%%% Jan. 8th Modified package name base date option
|
||||
%%% Jan. 22th Modified FmN and FmTi for error in book.cls
|
||||
%%% \MakeUppercase{#}->{\MakeUppercase#}
|
||||
%%% Apr. 6th Modified Lenny option to prevent undesired
|
||||
%%% skip of line.
|
||||
%%% Nov. 8th Fixed \@chapapp for AMS
|
||||
%%%
|
||||
%%% Revision 1.2 (1998)
|
||||
%%%
|
||||
%%% Feb. 11th Fixed appendix problem related to Bjarne
|
||||
%%% Aug. 11th Fixed problem related to 11pt and 12pt
|
||||
%%% suggested by Tomas Lundberg. THANKS!
|
||||
%%%
|
||||
%%% Revision 1.3 (2004)
|
||||
%%% Sep. 20th problem with frontmatter, mainmatter and
|
||||
%%% backmatter, pointed out by Lapo Mori
|
||||
%%%
|
||||
%%% Revision 1.31 (2004)
|
||||
%%% Sep. 21th problem with the Rejne definition streched text
|
||||
%%% caused ugly gaps in the vrule aligned with the title
|
||||
%%% text. Kindly pointed out to me by Hendri Adriaens
|
||||
%%%
|
||||
%%% Revision 1.32 (2005)
|
||||
%%% Jun. 23th compatibility problem with the KOMA class 'scrbook.cls'
|
||||
%%% a remedy is a redefinition of '\@schapter' in
|
||||
%%% line with that used in KOMA. The problem was pointed
|
||||
%%% out to me by Mikkel Holm Olsen
|
||||
%%%
|
||||
%%% Revision 1.33 (2005)
|
||||
%%% Aug. 9th misspelled ``TWELV'' corrected, the error was pointed
|
||||
%%% out to me by George Pearson
|
||||
%%%
|
||||
|
||||
|
||||
%%% Last modified Aug. 9th 2005
|
||||
|
||||
\NeedsTeXFormat{LaTeX2e}[1995/12/01]
|
||||
\ProvidesPackage{fncychap}
|
||||
[2004/09/21 v1.33
|
||||
LaTeX package (Revised chapters)]
|
||||
|
||||
%%%% DEFINITION OF Chapapp variables
|
||||
\newcommand{\CNV}{\huge\bfseries}
|
||||
\newcommand{\ChNameVar}[1]{\renewcommand{\CNV}{#1}}
|
||||
|
||||
|
||||
%%%% DEFINITION OF TheChapter variables
|
||||
\newcommand{\CNoV}{\huge\bfseries}
|
||||
\newcommand{\ChNumVar}[1]{\renewcommand{\CNoV}{#1}}
|
||||
|
||||
\newif\ifUCN
|
||||
\UCNfalse
|
||||
\newif\ifLCN
|
||||
\LCNfalse
|
||||
\def\ChNameLowerCase{\LCNtrue\UCNfalse}
|
||||
\def\ChNameUpperCase{\UCNtrue\LCNfalse}
|
||||
\def\ChNameAsIs{\UCNfalse\LCNfalse}
|
||||
|
||||
%%%%% Fix for AMSBook 971008
|
||||
|
||||
\@ifundefined{@chapapp}{\let\@chapapp\chaptername}{}
|
||||
|
||||
|
||||
%%%%% Fix for Bjarne and appendix 980211
|
||||
|
||||
\newif\ifinapp
|
||||
\inappfalse
|
||||
\renewcommand\appendix{\par
|
||||
\setcounter{chapter}{0}%
|
||||
\setcounter{section}{0}%
|
||||
\inapptrue%
|
||||
\renewcommand\@chapapp{\appendixname}%
|
||||
\renewcommand\thechapter{\@Alph\c@chapter}}
|
||||
|
||||
%%%%% Fix for frontmatter, mainmatter, and backmatter 040920
|
||||
|
||||
\@ifundefined{@mainmatter}{\newif\if@mainmatter \@mainmattertrue}{}
|
||||
|
||||
%%%%%
|
||||
|
||||
|
||||
|
||||
\newcommand{\FmN}[1]{%
|
||||
\ifUCN
|
||||
{\MakeUppercase#1}\LCNfalse
|
||||
\else
|
||||
\ifLCN
|
||||
{\MakeLowercase#1}\UCNfalse
|
||||
\else #1
|
||||
\fi
|
||||
\fi}
|
||||
|
||||
|
||||
%%%% DEFINITION OF Title variables
|
||||
\newcommand{\CTV}{\Huge\bfseries}
|
||||
\newcommand{\ChTitleVar}[1]{\renewcommand{\CTV}{#1}}
|
||||
|
||||
%%%% DEFINITION OF the basic rule width
|
||||
\newlength{\RW}
|
||||
\setlength{\RW}{1pt}
|
||||
\newcommand{\ChRuleWidth}[1]{\setlength{\RW}{#1}}
|
||||
|
||||
\newif\ifUCT
|
||||
\UCTfalse
|
||||
\newif\ifLCT
|
||||
\LCTfalse
|
||||
\def\ChTitleLowerCase{\LCTtrue\UCTfalse}
|
||||
\def\ChTitleUpperCase{\UCTtrue\LCTfalse}
|
||||
\def\ChTitleAsIs{\UCTfalse\LCTfalse}
|
||||
\newcommand{\FmTi}[1]{%
|
||||
\ifUCT
|
||||
{\MakeUppercase#1}\LCTfalse
|
||||
\else
|
||||
\ifLCT
|
||||
{\MakeLowercase#1}\UCTfalse
|
||||
\else {#1}
|
||||
\fi
|
||||
\fi}
|
||||
|
||||
|
||||
|
||||
\newlength{\mylen}
|
||||
\newlength{\myhi}
|
||||
\newlength{\px}
|
||||
\newlength{\py}
|
||||
\newlength{\pyy}
|
||||
\newlength{\pxx}
|
||||
|
||||
|
||||
\def\mghrulefill#1{\leavevmode\leaders\hrule\@height #1\hfill\kern\z@}
|
||||
|
||||
\newcommand{\DOCH}{%
|
||||
\CNV\FmN{\@chapapp}\space \CNoV\thechapter
|
||||
\par\nobreak
|
||||
\vskip 20\p@
|
||||
}
|
||||
\newcommand{\DOTI}[1]{%
|
||||
\CTV\FmTi{#1}\par\nobreak
|
||||
\vskip 40\p@
|
||||
}
|
||||
\newcommand{\DOTIS}[1]{%
|
||||
\CTV\FmTi{#1}\par\nobreak
|
||||
\vskip 40\p@
|
||||
}
|
||||
|
||||
%%%%%% SONNY DEF
|
||||
|
||||
\DeclareOption{Sonny}{%
|
||||
\ChNameVar{\Large\sf}
|
||||
\ChNumVar{\Huge}
|
||||
\ChTitleVar{\Large\sf}
|
||||
\ChRuleWidth{0.5pt}
|
||||
\ChNameUpperCase
|
||||
\renewcommand{\DOCH}{%
|
||||
\raggedleft
|
||||
\CNV\FmN{\@chapapp}\space \CNoV\thechapter
|
||||
\par\nobreak
|
||||
\vskip 40\p@}
|
||||
\renewcommand{\DOTI}[1]{%
|
||||
\CTV\raggedleft\mghrulefill{\RW}\par\nobreak
|
||||
\vskip 5\p@
|
||||
\CTV\FmTi{#1}\par\nobreak
|
||||
\mghrulefill{\RW}\par\nobreak
|
||||
\vskip 40\p@}
|
||||
\renewcommand{\DOTIS}[1]{%
|
||||
\CTV\raggedleft\mghrulefill{\RW}\par\nobreak
|
||||
\vskip 5\p@
|
||||
\CTV\FmTi{#1}\par\nobreak
|
||||
\mghrulefill{\RW}\par\nobreak
|
||||
\vskip 40\p@}
|
||||
}
|
||||
|
||||
%%%%%% LENNY DEF
|
||||
|
||||
\DeclareOption{Lenny}{%
|
||||
|
||||
\ChNameVar{\fontsize{14}{16}\usefont{OT1}{phv}{m}{n}\selectfont}
|
||||
\ChNumVar{\fontsize{60}{62}\usefont{OT1}{ptm}{m}{n}\selectfont}
|
||||
\ChTitleVar{\Huge\bfseries\rm}
|
||||
\ChRuleWidth{1pt}
|
||||
\renewcommand{\DOCH}{%
|
||||
\settowidth{\px}{\CNV\FmN{\@chapapp}}
|
||||
\addtolength{\px}{2pt}
|
||||
\settoheight{\py}{\CNV\FmN{\@chapapp}}
|
||||
\addtolength{\py}{1pt}
|
||||
|
||||
\settowidth{\mylen}{\CNV\FmN{\@chapapp}\space\CNoV\thechapter}
|
||||
\addtolength{\mylen}{1pt}
|
||||
\settowidth{\pxx}{\CNoV\thechapter}
|
||||
\addtolength{\pxx}{-1pt}
|
||||
|
||||
\settoheight{\pyy}{\CNoV\thechapter}
|
||||
\addtolength{\pyy}{-2pt}
|
||||
\setlength{\myhi}{\pyy}
|
||||
\addtolength{\myhi}{-1\py}
|
||||
\par
|
||||
\parbox[b]{\textwidth}{%
|
||||
\rule[\py]{\RW}{\myhi}%
|
||||
\hskip -\RW%
|
||||
\rule[\pyy]{\px}{\RW}%
|
||||
\hskip -\px%
|
||||
\raggedright%
|
||||
\CNV\FmN{\@chapapp}\space\CNoV\thechapter%
|
||||
\hskip1pt%
|
||||
\mghrulefill{\RW}%
|
||||
\rule{\RW}{\pyy}\par\nobreak%
|
||||
\vskip -\baselineskip%
|
||||
\vskip -\pyy%
|
||||
\hskip \mylen%
|
||||
\mghrulefill{\RW}\par\nobreak%
|
||||
\vskip \pyy}%
|
||||
\vskip 20\p@}
|
||||
|
||||
|
||||
\renewcommand{\DOTI}[1]{%
|
||||
\raggedright
|
||||
\CTV\FmTi{#1}\par\nobreak
|
||||
\vskip 40\p@}
|
||||
|
||||
\renewcommand{\DOTIS}[1]{%
|
||||
\raggedright
|
||||
\CTV\FmTi{#1}\par\nobreak
|
||||
\vskip 40\p@}
|
||||
}
|
||||
|
||||
|
||||
%%%%%%% GLENN DEF
|
||||
|
||||
|
||||
\DeclareOption{Glenn}{%
|
||||
\ChNameVar{\bfseries\Large\sf}
|
||||
\ChNumVar{\Huge}
|
||||
\ChTitleVar{\bfseries\Large\rm}
|
||||
\ChRuleWidth{1pt}
|
||||
\ChNameUpperCase
|
||||
\ChTitleUpperCase
|
||||
\renewcommand{\DOCH}{%
|
||||
\settoheight{\myhi}{\CTV\FmTi{Test}}
|
||||
\setlength{\py}{\baselineskip}
|
||||
\addtolength{\py}{\RW}
|
||||
\addtolength{\py}{\myhi}
|
||||
\setlength{\pyy}{\py}
|
||||
\addtolength{\pyy}{-1\RW}
|
||||
|
||||
\raggedright
|
||||
\CNV\FmN{\@chapapp}\space\CNoV\thechapter
|
||||
\hskip 3pt\mghrulefill{\RW}\rule[-1\pyy]{2\RW}{\py}\par\nobreak}
|
||||
|
||||
\renewcommand{\DOTI}[1]{%
|
||||
\addtolength{\pyy}{-4pt}
|
||||
\settoheight{\myhi}{\CTV\FmTi{#1}}
|
||||
\addtolength{\myhi}{\py}
|
||||
\addtolength{\myhi}{-1\RW}
|
||||
\vskip -1\pyy
|
||||
\rule{2\RW}{\myhi}\mghrulefill{\RW}\hskip 2pt
|
||||
\raggedleft\CTV\FmTi{#1}\par\nobreak
|
||||
\vskip 80\p@}
|
||||
|
||||
\newlength{\backskip}
|
||||
\renewcommand{\DOTIS}[1]{%
|
||||
% \setlength{\py}{10pt}
|
||||
% \setlength{\pyy}{\py}
|
||||
% \addtolength{\pyy}{\RW}
|
||||
% \setlength{\myhi}{\baselineskip}
|
||||
% \addtolength{\myhi}{\pyy}
|
||||
% \mghrulefill{\RW}\rule[-1\py]{2\RW}{\pyy}\par\nobreak
|
||||
% \addtolength{}{}
|
||||
%\vskip -1\baselineskip
|
||||
% \rule{2\RW}{\myhi}\mghrulefill{\RW}\hskip 2pt
|
||||
% \raggedleft\CTV\FmTi{#1}\par\nobreak
|
||||
% \vskip 60\p@}
|
||||
%% Fix suggested by Tomas Lundberg
|
||||
\setlength{\py}{25pt} % eller vad man vill
|
||||
\setlength{\pyy}{\py}
|
||||
\setlength{\backskip}{\py}
|
||||
\addtolength{\backskip}{2pt}
|
||||
\addtolength{\pyy}{\RW}
|
||||
\setlength{\myhi}{\baselineskip}
|
||||
\addtolength{\myhi}{\pyy}
|
||||
\mghrulefill{\RW}\rule[-1\py]{2\RW}{\pyy}\par\nobreak
|
||||
\vskip -1\backskip
|
||||
\rule{2\RW}{\myhi}\mghrulefill{\RW}\hskip 3pt %
|
||||
\raggedleft\CTV\FmTi{#1}\par\nobreak
|
||||
\vskip 40\p@}
|
||||
}
|
||||
|
||||
%%%%%%% CONNY DEF
|
||||
|
||||
\DeclareOption{Conny}{%
|
||||
\ChNameUpperCase
|
||||
\ChTitleUpperCase
|
||||
\ChNameVar{\centering\Huge\rm\bfseries}
|
||||
\ChNumVar{\Huge}
|
||||
\ChTitleVar{\centering\Huge\rm}
|
||||
\ChRuleWidth{2pt}
|
||||
|
||||
\renewcommand{\DOCH}{%
|
||||
\mghrulefill{3\RW}\par\nobreak
|
||||
\vskip -0.5\baselineskip
|
||||
\mghrulefill{\RW}\par\nobreak
|
||||
\CNV\FmN{\@chapapp}\space \CNoV\thechapter
|
||||
\par\nobreak
|
||||
\vskip -0.5\baselineskip
|
||||
}
|
||||
\renewcommand{\DOTI}[1]{%
|
||||
\mghrulefill{\RW}\par\nobreak
|
||||
\CTV\FmTi{#1}\par\nobreak
|
||||
\vskip 60\p@
|
||||
}
|
||||
\renewcommand{\DOTIS}[1]{%
|
||||
\mghrulefill{\RW}\par\nobreak
|
||||
\CTV\FmTi{#1}\par\nobreak
|
||||
\vskip 60\p@
|
||||
}
|
||||
}
|
||||
|
||||
%%%%%%% REJNE DEF
|
||||
|
||||
\DeclareOption{Rejne}{%
|
||||
|
||||
\ChNameUpperCase
|
||||
\ChTitleUpperCase
|
||||
\ChNameVar{\centering\Large\rm}
|
||||
\ChNumVar{\Huge}
|
||||
\ChTitleVar{\centering\Huge\rm}
|
||||
\ChRuleWidth{1pt}
|
||||
\renewcommand{\DOCH}{%
|
||||
\settoheight{\py}{\CNoV\thechapter}
|
||||
\parskip=0pt plus 1pt % Set parskip to default, just in case v1.31
|
||||
\addtolength{\py}{-1pt}
|
||||
\CNV\FmN{\@chapapp}\par\nobreak
|
||||
\vskip 20\p@
|
||||
\setlength{\myhi}{2\baselineskip}
|
||||
\setlength{\px}{\myhi}
|
||||
\addtolength{\px}{-1\RW}
|
||||
\rule[-1\px]{\RW}{\myhi}\mghrulefill{\RW}\hskip
|
||||
10pt\raisebox{-0.5\py}{\CNoV\thechapter}\hskip 10pt\mghrulefill{\RW}\rule[-1\px]{\RW}{\myhi}\par\nobreak
|
||||
\vskip -3\p@% Added -2pt vskip to correct for streched text v1.31
|
||||
}
|
||||
\renewcommand{\DOTI}[1]{%
|
||||
\setlength{\mylen}{\textwidth}
|
||||
\parskip=0pt plus 1pt % Set parskip to default, just in case v1.31
|
||||
\addtolength{\mylen}{-2\RW}
|
||||
{\vrule width\RW}\parbox{\mylen}{\CTV\FmTi{#1}}{\vrule width\RW}\par\nobreak%
|
||||
\vskip -3pt\rule{\RW}{2\baselineskip}\mghrulefill{\RW}\rule{\RW}{2\baselineskip}%
|
||||
\vskip 60\p@% Added -2pt in vskip to correct for streched text v1.31
|
||||
}
|
||||
\renewcommand{\DOTIS}[1]{%
|
||||
\setlength{\py}{\fboxrule}
|
||||
\setlength{\fboxrule}{\RW}
|
||||
\setlength{\mylen}{\textwidth}
|
||||
\addtolength{\mylen}{-2\RW}
|
||||
\fbox{\parbox{\mylen}{\vskip 2\baselineskip\CTV\FmTi{#1}\par\nobreak\vskip \baselineskip}}
|
||||
\setlength{\fboxrule}{\py}
|
||||
\vskip 60\p@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
%%%%%%% BJARNE DEF
|
||||
|
||||
\DeclareOption{Bjarne}{%
|
||||
\ChNameUpperCase
|
||||
\ChTitleUpperCase
|
||||
\ChNameVar{\raggedleft\normalsize\rm}
|
||||
\ChNumVar{\raggedleft \bfseries\Large}
|
||||
\ChTitleVar{\raggedleft \Large\rm}
|
||||
\ChRuleWidth{1pt}
|
||||
|
||||
|
||||
%% Note thechapter -> c@chapter fix appendix bug
|
||||
%% Fixed misspelled 12
|
||||
|
||||
\newcounter{AlphaCnt}
|
||||
\newcounter{AlphaDecCnt}
|
||||
\newcommand{\AlphaNo}{%
|
||||
\ifcase\number\theAlphaCnt
|
||||
\ifnum\c@chapter=0
|
||||
ZERO\else{}\fi
|
||||
\or ONE\or TWO\or THREE\or FOUR\or FIVE
|
||||
\or SIX\or SEVEN\or EIGHT\or NINE\or TEN
|
||||
\or ELEVEN\or TWELVE\or THIRTEEN\or FOURTEEN\or FIFTEEN
|
||||
\or SIXTEEN\or SEVENTEEN\or EIGHTEEN\or NINETEEN\fi
|
||||
}
|
||||
|
||||
\newcommand{\AlphaDecNo}{%
|
||||
\setcounter{AlphaDecCnt}{0}
|
||||
\@whilenum\number\theAlphaCnt>0\do
|
||||
{\addtocounter{AlphaCnt}{-10}
|
||||
\addtocounter{AlphaDecCnt}{1}}
|
||||
\ifnum\number\theAlphaCnt=0
|
||||
\else
|
||||
\addtocounter{AlphaDecCnt}{-1}
|
||||
\addtocounter{AlphaCnt}{10}
|
||||
\fi
|
||||
|
||||
|
||||
\ifcase\number\theAlphaDecCnt\or TEN\or TWENTY\or THIRTY\or
|
||||
FORTY\or FIFTY\or SIXTY\or SEVENTY\or EIGHTY\or NINETY\fi
|
||||
}
|
||||
\newcommand{\TheAlphaChapter}{%
|
||||
|
||||
\ifinapp
|
||||
\thechapter
|
||||
\else
|
||||
\setcounter{AlphaCnt}{\c@chapter}
|
||||
\ifnum\c@chapter<20
|
||||
\AlphaNo
|
||||
\else
|
||||
\AlphaDecNo\AlphaNo
|
||||
\fi
|
||||
\fi
|
||||
}
|
||||
\renewcommand{\DOCH}{%
|
||||
\mghrulefill{\RW}\par\nobreak
|
||||
\CNV\FmN{\@chapapp}\par\nobreak
|
||||
\CNoV\TheAlphaChapter\par\nobreak
|
||||
\vskip -1\baselineskip\vskip 5pt\mghrulefill{\RW}\par\nobreak
|
||||
\vskip 20\p@
|
||||
}
|
||||
\renewcommand{\DOTI}[1]{%
|
||||
\CTV\FmTi{#1}\par\nobreak
|
||||
\vskip 40\p@
|
||||
}
|
||||
\renewcommand{\DOTIS}[1]{%
|
||||
\CTV\FmTi{#1}\par\nobreak
|
||||
\vskip 40\p@
|
||||
}
|
||||
}
|
||||
|
||||
\DeclareOption*{%
|
||||
\PackageWarning{fancychapter}{unknown style option}
|
||||
}
|
||||
|
||||
\ProcessOptions* \relax
|
||||
|
||||
\def\@makechapterhead#1{%
|
||||
\vspace*{50\p@}%
|
||||
{\parindent \z@ \raggedright \normalfont
|
||||
\ifnum \c@secnumdepth >\m@ne
|
||||
\if@mainmatter%%%%% Fix for frontmatter, mainmatter, and backmatter 040920
|
||||
\DOCH
|
||||
\fi
|
||||
\fi
|
||||
\interlinepenalty\@M
|
||||
\DOTI{#1}
|
||||
}}
|
||||
|
||||
|
||||
%%% Begin: To avoid problem with scrbook.cls (fncychap version 1.32)
|
||||
|
||||
%%OUT:
|
||||
%\def\@schapter#1{\if@twocolumn
|
||||
% \@topnewpage[\@makeschapterhead{#1}]%
|
||||
% \else
|
||||
% \@makeschapterhead{#1}%
|
||||
% \@afterheading
|
||||
% \fi}
|
||||
|
||||
%%IN:
|
||||
\def\@schapter#1{%
|
||||
\if@twocolumn%
|
||||
\@makeschapterhead{#1}%
|
||||
\else%
|
||||
\@makeschapterhead{#1}%
|
||||
\@afterheading%
|
||||
\fi}
|
||||
|
||||
%%% End: To avoid problem with scrbook.cls (fncychap version 1.32)
|
||||
|
||||
\def\@makeschapterhead#1{%
|
||||
\vspace*{50\p@}%
|
||||
{\parindent \z@ \raggedright
|
||||
\normalfont
|
||||
\interlinepenalty\@M
|
||||
\DOTIS{#1}
|
||||
\vskip 40\p@
|
||||
}}
|
||||
|
||||
\endinput
|
||||
|
||||
|
|
@ -0,0 +1,469 @@
|
|||
==================================
|
||||
w.c.s. - Guide de l'administrateur
|
||||
==================================
|
||||
|
||||
:auteur: Christophe Boutet et Pierre Cros
|
||||
:contact: cboutet@entrouvert.com
|
||||
:contact: pcros@entrouvert.com
|
||||
:copyright: Copyright © 2005-2006 Entr'ouvert
|
||||
|
||||
.. contents:: Table des matières
|
||||
.. section-numbering::
|
||||
|
||||
Vue générale
|
||||
============
|
||||
|
||||
w.c.s est un logiciel permettant de générer des formulaires et des consultations
|
||||
en ligne et de les intégrer dans un workflow. Il est conforme aux standards et
|
||||
protocoles du consortium `Liberty Alliance`_ grâce à l'utilisation de la
|
||||
librairie certifiée Lasso_. Il dispose d'une interface d'administration et d'un
|
||||
back-office soignés permettant une personnalisation poussée et son adaption à de
|
||||
nombreux usages différents.
|
||||
|
||||
Se procurer et installer w.c.s.
|
||||
===============================
|
||||
|
||||
Installation sous Debian_ Sarge
|
||||
+++++++++++++++++++++++++++++++
|
||||
|
||||
Pour fonctionner correctement Authentic s'appuie sur :
|
||||
|
||||
* Apache_ (1.3 ou 2, Apache2 recommandé) ;
|
||||
|
||||
* Lasso_ (0.6.2) ;
|
||||
|
||||
* Quixote_ (2.0) ;
|
||||
|
||||
* mod_python_ ou SCGI_ (SCGI_ recommandé).
|
||||
|
||||
|
||||
Installation des paquets
|
||||
------------------------
|
||||
|
||||
En tant que root tapez la commande ::
|
||||
|
||||
echo 'deb http://deb.entrouvert.org/ sarge main' \
|
||||
>> /etc/apt/sources.list
|
||||
|
||||
Cette commande ajoute le répertoire qui contient tous les paquets nécessaires
|
||||
dans votre fichier sources.list.
|
||||
|
||||
Toujours en tant que root tapez ::
|
||||
|
||||
apt-get update
|
||||
apt-get install wcs
|
||||
|
||||
Tous les paquets nécessaires sont installés.
|
||||
|
||||
Si vous ne souhaitez pas modifier votre fichier sources.list vous pouvez
|
||||
récupérer les paquets nécessaire et les installer manuellement avec la commande
|
||||
dpkg -i :
|
||||
|
||||
* wcs et Quixote 2.0 sur http://wcs.labs.libre-entreprise.org/ ;
|
||||
|
||||
* Lasso sur http://lasso.entrouvert.org.
|
||||
|
||||
Configuration d'Apache_
|
||||
-----------------------
|
||||
|
||||
Il faut ensuite configurer Apache_ pour avoir un virtual host w.c.s., le
|
||||
fichier d'example ci-dessous s'appelle vhost-apache-wcs et il est installé par
|
||||
défaut. Il fonctionne (en remplaçant www.example.com par le nom de domaine que
|
||||
vous avez choisi pour w.c.s., nous utiliserons wcs.example.com) pour Apache2 et
|
||||
SCGI_. Vous le trouverez dans le répertoire ``/etc/apache2/sites-enabled`` ::
|
||||
|
||||
<VirtualHost *>
|
||||
ServerAdmin webmaster@locahost
|
||||
ServerName wcs.example.com
|
||||
DocumentRoot /usr/share/wcs/web/
|
||||
<LocationMatch "^/(forms|admin|liberty|login|logout|themes|consultations|token)|^/$">
|
||||
SCGIServer 127.0.0.1:3001
|
||||
SCGIHandler On
|
||||
</LocationMatch>
|
||||
SSLEngine On
|
||||
CustomLog /var/log/apache2/wcs-access.log combined
|
||||
ErrorLog /var/log/apache2/wcs-error.log
|
||||
</VirtualHost>
|
||||
|
||||
Il faut également vous assurer qu'Apache_ est configuré pour supporter le SSL,
|
||||
vérifiez que dans votre fichier /etc/apache2/ports.conf vous avez une ligne ::
|
||||
|
||||
Listen 443
|
||||
|
||||
Ajoutez la si elle n'est pas présente.
|
||||
|
||||
Ensuite, il s'agit d'activer le module SCGI, s'il ne l'était déjà ::
|
||||
|
||||
a2enmod scgi
|
||||
|
||||
Vous pouvez ensuite redémarrer Apache_ (toujours en root) ::
|
||||
|
||||
/etc/init.d/apache2 restart
|
||||
|
||||
Pensez également à modifier votre fichier /etc/hosts le cas échéant. Lorsque wcs
|
||||
fonctionne, l'interface d'administration est se trouve à l'URL
|
||||
http://wcs.example.com/admin.
|
||||
|
||||
Installation avec une autre distribution Linux
|
||||
++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
Nous supposons qu'Apache_, SCGI_ ou mod_python_ sont déjà installés. Il faut
|
||||
ensuite télécharger les sources suivantes et les installer :
|
||||
|
||||
* Lasso http://lasso.entrouvert.org ;
|
||||
|
||||
* Quixote http://www.mems-exchange.org/software/quixote/ ;
|
||||
|
||||
* Authentic http://authentic.labs.libre-entreprise.org/.
|
||||
|
||||
Pour installer Authentic, décompressez les sources que vous avez téléchargées
|
||||
et lancez le script setup.py ::
|
||||
|
||||
tar xzf wcs*.tar.gz
|
||||
cd wcs*
|
||||
python setup.py install
|
||||
|
||||
Il vous faut ensuite configurer correctement Apache_.
|
||||
|
||||
Lorsque que w.c.s. fonctionne, l'interface d'administration est accessible à
|
||||
l'URL http://wcs.example.com/admin.
|
||||
|
||||
Installation sous Windows
|
||||
+++++++++++++++++++++++++
|
||||
|
||||
Nous n'avons pas à l'heure actuelle réalisé d'installation de w.c.s. sous
|
||||
Windows. Mais étant donné que tous les composants nécessaires à son utilisation
|
||||
fonctionne sur ce système d'exploitation, l'installation est envisageable et
|
||||
nous seront peut-être amenés à la décrire bientôt. N'hésitez pas à nous faire
|
||||
part de vos tentatives.
|
||||
|
||||
Configuration de base de wcs
|
||||
============================
|
||||
|
||||
Création clés publiques et privées
|
||||
++++++++++++++++++++++++++++++++++
|
||||
|
||||
Si vous ne possédez pas de clés au format pem, il vous faut en créer car elles
|
||||
seront nécessaire pour configurer wcs comme fournisseur de service. Pour créer
|
||||
un couple clé publique/clé privée avec OpenSSL_, utilisez ces commandes ::
|
||||
|
||||
openssl genrsa -out nom-de-la-clé-privé.pem 2048
|
||||
|
||||
Cette commande crée la clé privée sous la forme d'un fichier appelé
|
||||
nom-de-la-clé-privé.pem. ::
|
||||
|
||||
openssl rsa -in nom-de-la-clé-privé-key.pem -pubout\
|
||||
-out nom-de-la-clé-publique.pem
|
||||
|
||||
Cette commande extrait la clé publique de la clé privée sous la forme d'un
|
||||
fichier appelé nom-de-la-clé-publique.pem.
|
||||
|
||||
Configuration de base du fournisseur de service
|
||||
+++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
Allez sur l'interface d'administration de w.c.s. http://wcs.example.com/admin.
|
||||
|
||||
.. figure:: figures/wcs-admin.png
|
||||
|
||||
L'interface d'administration lorsqu'aucun utilisateur n'existe encore.
|
||||
|
||||
Cliquez sur l'onglet paramètres puis sur le lien « fournisseur de service ».
|
||||
|
||||
.. figure:: figures/wcs-admin-settings-liberty_sp.png
|
||||
|
||||
Configuration du fournisseur de service wcs
|
||||
|
||||
Les deux premiers champs sont remplis automatiquement, ne cherchez pas à les
|
||||
modifier à moins de savoir réellement ce que vous faites.
|
||||
|
||||
Champs :
|
||||
|
||||
* Identifiant du fournisseur (un identifiant qui prend nécessairement la
|
||||
forme d'une URL) ;
|
||||
|
||||
* URL de la racine (toutes les URL nécessaires à `Liberty Alliance`_ se
|
||||
trouvent sur cette racine) ;
|
||||
|
||||
* Nom de l'organisation (nom de l'organisation qui gère le fournisseur
|
||||
d'identité) ;
|
||||
|
||||
* Clé privée (clé privée au format pem) ;
|
||||
|
||||
* Clé publique (clé publique au format pem) ;
|
||||
|
||||
* Domaine commun, pour « Identity Provider Introduction » (L'identity
|
||||
provider introduction est un mécanisme `Liberty Alliance`_ permettant à un
|
||||
fournisseur d'identité, pour un nom de domaine particulier, de générer un
|
||||
cookie sur la machine de l'utilisateur. C'est utile lorsqu'il y a plusieurs
|
||||
fournisseurs d'identités associés à un fournisseur de service : dans le
|
||||
cookie on associe les fournisseurs de service d'un domaine, au fournisseur
|
||||
d'identité qui a délivré le cookie. Cela permet de stipuler au fournisseur de
|
||||
service : « cet utilisateur utilise le fournisseur d'identité du domaine
|
||||
».).
|
||||
|
||||
Une fois tous ces champs dûment remplis, cliquez sur le bouton valider.
|
||||
|
||||
Enregistrement du fichier de metadata
|
||||
+++++++++++++++++++++++++++++++++++++
|
||||
|
||||
Dans l'interface d'administration de wcs vous pouvez récupérer le
|
||||
fichier de metadata. Cela sera utile par la suite lorsqu'il s'agira de
|
||||
déclarer wcs fournisseur de service sur un fournisseur d'identité. Procédez
|
||||
comme suit :
|
||||
|
||||
* cliquez sur l'onglet paramètres ;
|
||||
|
||||
* vous voyez un lien « Metadata du fournisseur de service ». Faites un clic
|
||||
droit et « enregistrer la cible du lien sous » ;
|
||||
|
||||
* choisissez le nom que vous donnez à ce fichier (par exemple
|
||||
metadata-wcs.xml) et l'endroit ou vous le sauvegardez.
|
||||
|
||||
Déclarer un fournisseur d'identité
|
||||
++++++++++++++++++++++++++++++++++
|
||||
|
||||
Sur l'interface d'administration de w.c.s., cliquez sur l'onglet
|
||||
paramètres, puis sur le lien « fournisseurs d'identités ». Cliquez encore
|
||||
sur « nouveau ».
|
||||
|
||||
.. figure:: figures/wcs-admin-settings-liberty_idp-new.png
|
||||
|
||||
Déclarer un fournisseur d'identité
|
||||
|
||||
Compléter les champs suivants :
|
||||
|
||||
* Metadata (le fichier de metadata du fournisseur d'identité) ;
|
||||
|
||||
* Clé publique (la clé publique du fournisseur d'identité) ;
|
||||
|
||||
* Chaîne de certification (certificat contenant toute la chaîne
|
||||
d'authentification jusqu'au root CA).
|
||||
|
||||
Création Administrateur
|
||||
+++++++++++++++++++++++
|
||||
|
||||
Les paramètres Liberty doivent avoir été configurés préablablement à la création
|
||||
des utilisateurs. Pour créer l'administrateur, allez sur l'interface
|
||||
d'administration de w.c.s.. Cliquez sur l'onglet « Gestion des identités », puis
|
||||
sur le lien « Ajouter une identité ».
|
||||
|
||||
.. figure:: figures/wcs-admin-users-new.png
|
||||
|
||||
Création de la première identité, celle de l'administrateur
|
||||
|
||||
Remplissez les champs suivants :
|
||||
|
||||
* Nom (saisissez vos Nom et prénom) ;
|
||||
|
||||
* Courriel (saisissez votre Courriel) ;
|
||||
|
||||
* Compte administrateur (cochez cette case pour que le compte créé soit un
|
||||
compte administrateur) ;
|
||||
|
||||
Cliquez sur valider, le compte administrateur est créé.
|
||||
|
||||
Onglet rôles
|
||||
============
|
||||
|
||||
À chaque rôle il faut affecter une ou plusieurs adresses mail, qui seront
|
||||
destinataires des notifications de remplissage de formulaires par les
|
||||
utilisateurs.
|
||||
|
||||
Onglet utilisateurs
|
||||
===================
|
||||
|
||||
Une fois un rôle créé, il faut aller sur les utilisateurs qui y auront accès au
|
||||
back-office correspondant et leur affecter le rôle.
|
||||
|
||||
On peut également grâce à cet onglet créer des utilisateurs, mais la création
|
||||
d'un utilisateur à ce niveau n'est pas suffisante, s'agissant d'un contexte
|
||||
Liberty Alliance, il faut également que l'utilisateur dispose d'un compte sur le
|
||||
fournisseur d'identité.
|
||||
|
||||
Onglet catégories
|
||||
=================
|
||||
|
||||
Il permet de gérer les catégories dans lesquelles les formulaires seront rangés
|
||||
coté utilisateur, par exemple « vie pratique ».
|
||||
|
||||
Par défaut si la catégorie de rangement n'est pas choisie pour un formulaire,
|
||||
il est classé dans la catégorie divers.
|
||||
|
||||
Onglet formulaires
|
||||
==================
|
||||
|
||||
il permet de créer et d'administrer les formulaires :
|
||||
|
||||
Création du formulaire
|
||||
++++++++++++++++++++++
|
||||
|
||||
Cliquer sur nouveau
|
||||
|
||||
Donner un nom au formulaire, puis fixer la valeur des champs et les nommer:
|
||||
Les champs : titre, sous titre, et commentaire, ne sont pas de champs de
|
||||
réponse, ils servent à apporter des compléments d'information sur les
|
||||
formulaires.
|
||||
|
||||
Le champ commentaire, en particulier, permet de préciser le caractère
|
||||
obligatoire des réponses sur certaines question, matérialisé par un carré rouge,
|
||||
mais aussi tout type d'information utiles, en revanche, il n'a pas fonction à
|
||||
servir d'aide directe à la complétion d'une question (traitée par ailleurs).
|
||||
|
||||
Les autres champs fixent la nature de la réponse que l'on souhaite voir
|
||||
apportée : date, adresse mail, bloc de texte, ligne de texte (qui sert également
|
||||
pour les chiffres), case à cocher, liste, upload de fichier.
|
||||
|
||||
Une fois les champs fixés, il faut choisir le destinataire du formulaire.
|
||||
|
||||
Ensuite on affecte un rôle au formulaire, par défaut un formulaire est
|
||||
accessible à tout le monde, on peut restreindre l'accès de manière fine par le
|
||||
biais des rôles.
|
||||
|
||||
La catégorie permet de classer l'affichage du formulaire coté utilisateur.
|
||||
|
||||
Il reste alors une série d'options :
|
||||
|
||||
- Cochée, la case « Inclure une page de confirmation » permet d'afficher une page
|
||||
récapitulative coté utilisateur avant qu'il n'envoie le formulaire.
|
||||
|
||||
- Cochée, la case « Permettre la discussion » autorise un dialogue via le
|
||||
back-office entre l'utilisateur et la personne en charge du traitement d'un
|
||||
formulaire.
|
||||
|
||||
- Cochée, la case « accès public » permet à tous les utilisateurs d'un formulaire
|
||||
de visualiser toutes les réponses apportées.
|
||||
|
||||
- Cochée, la case « Envoyer des courriels de notification détaillés » , génère
|
||||
pour la personne chargée du traitement, un mail reprenant tous les champs
|
||||
complétés par le demandeur.
|
||||
|
||||
- Cochée, la case « Désactiver l'accès au formulaire » ne permet plus la
|
||||
visualisation, donc la complétion du formulaire concerné coté utilisateur.
|
||||
Cette option permet de conserver dans la base les formulaires à utilisation
|
||||
saisonnière.
|
||||
|
||||
La validation en bas de page génère la création du formulaire et vous dirige
|
||||
vers l'écran suivant qui va vous permettre de définir précisement les champs
|
||||
selon leur type.
|
||||
|
||||
Définition des champs
|
||||
+++++++++++++++++++++
|
||||
|
||||
Outre la définition des champs, vous pouvez depuis cet écran modifier leur ordre
|
||||
par « drag&drop ».
|
||||
|
||||
Tous les types de champs disposent d'une série d'options communes:
|
||||
|
||||
Obligatoire: il s'agit d'une case à cocher qui fixe le caractère obligatoire
|
||||
d'une réponse pour l'utilisateur, si la case n'est pas cochée, la réponse à la
|
||||
question est optionnelle.
|
||||
|
||||
Affichage dans les Listings: il n'est pas forcément pertinent que tous les
|
||||
champs figurent dans le listing de back-office d'autant que dans le cas de
|
||||
formulaires comprenant beaucoup de champs, le listing n'est pas très lisible si
|
||||
tous sont affichés. Cette option permet donc, par le biais d'une case à cocher
|
||||
de fixer, ou non, l'affichage du champ concerné dans le listing back-office.
|
||||
|
||||
Remarque: permet d'apporter une aide au répondant, dans le cas d'un champ
|
||||
adresse électronique: Exemple: francis.kuntz@wanagro.com.
|
||||
|
||||
Champ date : Il oblige le répondant à compléter une date de la forme 12/12/2005,
|
||||
dans le cas des mois ou jours à chiffre unique, le zéro n'est pas obligatoire,
|
||||
3/6/2005 est admis.
|
||||
|
||||
Les dates admises actuellement vont du 01/01/1800 au 31/12/2099, un contrôle est
|
||||
opéré.
|
||||
|
||||
Champ adresse électronique : le répondant devra mentionner une adresse mail, la
|
||||
vérification s'effectue sur l'arobase.
|
||||
|
||||
Champ bloc de texte : la taille du bloc de texte est modifiable, par défaut le
|
||||
nombre de caractères par ligne est fixé à 20 et le nombre de lignes à 3. Pour un
|
||||
affichage optimal coté utilisateur, 70 caractères par ligne constitue un bon
|
||||
compromis. 10 lignes par bloc permet une réponse déjà longue, sachant que si
|
||||
l'utilisateur dépasse, il aura un ascenseur.
|
||||
|
||||
Champ ligne de texte: Par défaut le nombre de caractères d'une ligne est fixé à
|
||||
20, Le champ texte permet, de base une réponse comprenant des lettres et des
|
||||
chiffres, si la réponse ne doit comporter que des chiffres, il convient
|
||||
d'appliquer une règle dans le champ regex prévu à cet effet.
|
||||
|
||||
Exemple de règles :
|
||||
|
||||
- Tél : 10 chiffres ``^\d{10}$``
|
||||
- Code postal : 5 chiffres ``^\d{5}$``
|
||||
- Valeur numérique ``^\d+$``
|
||||
|
||||
La valeur Liberty, si elle est complétée autorise le pré-remplissage.
|
||||
Si vous souhaitez utiliser cette fonction, il est indispensable que vous mettiez
|
||||
en place un fournisseur de service candle.
|
||||
La case explicite, si elle est cochée, demande un consentement supplémentaire à
|
||||
l'utilisateur pour la complétion.
|
||||
|
||||
Champ liste : grâce à « éléments », il vous faut fixer possibilités de réponse à
|
||||
votre liste, ajoutez autant d'éléments que nécessaire.
|
||||
|
||||
Vous pouvez choisir d'afficher les réponses sous forme de bouton radio à l'aide
|
||||
de la case à cocher prévue à cet effet.
|
||||
|
||||
Champ case à cocher : Case à cocher ; Un formulaire, une fois créé apparaît
|
||||
sous la forme d'un résumé d'une ligne en bout de laquelle sont affichés 4
|
||||
boutons qui respectivement permettent: l'édition, la modification des champs,
|
||||
la duplication et la suppression.
|
||||
|
||||
|
||||
Éditer
|
||||
++++++
|
||||
|
||||
Le bouton prévu à cet effet permet d'éditer le formulaire.
|
||||
Vous pourrez ensuite modifier le cas échéant, les types de champ du formulaire
|
||||
concerné, ainsi que les rôles, catégories, destinataire et d'activer/désactiver
|
||||
le formulaire.
|
||||
|
||||
Modifier
|
||||
++++++++
|
||||
|
||||
Grâce à ce bouton, vous pourrez ensuite modifier les paramètres des champs d'un
|
||||
formulaire ainsi que les déplacer par « drag&drop ».
|
||||
|
||||
Dupliquer
|
||||
+++++++++
|
||||
|
||||
Ce bouton permet la duplication d'un formulaire, pour éviter d'avoir à faire une
|
||||
création ex-nihilo si vous souhaiter créer un formulaire ayant une structuration
|
||||
proche d'un existant.
|
||||
|
||||
Supprimer
|
||||
+++++++++
|
||||
|
||||
Vous pourrez ici supprimer un formulaire. Afin d'éviter les suppressions
|
||||
brutales ou les erreurs de manipulation, une confirmation de la suppression
|
||||
est demandée.
|
||||
|
||||
Onglet Logs
|
||||
===========
|
||||
|
||||
S'il est activé, permet d'analyser le comportement des répondants.
|
||||
|
||||
Onglet Paramètres
|
||||
=================
|
||||
|
||||
Sert à paramétrer complètement l'application
|
||||
|
||||
|
||||
|
||||
Licences
|
||||
========
|
||||
|
||||
w.c.s., Authentic_, et Lasso_ sont publiés sous la `licence GNU/GPL`_.
|
||||
|
||||
.. _Lasso: http://lasso.entrouvert.org/
|
||||
.. _`licence GNU/GPL`: http://www.gnu.org/copyleft/gpl.html
|
||||
.. _`Liberty Alliance`: http://projectliberty.org/
|
||||
.. _Authentic: http://authentic.labs.libre-entreprise.org
|
||||
.. _Debian: http://www.debian.org/
|
||||
.. _Apache: http://www.apache.org/
|
||||
.. _Quixote: http://www.mems-exchange.org/software/Quixote
|
||||
.. _mod_python: http://www.modpython.org/
|
||||
.. _SCGI: http://www.mems-exchange.org/software/scgi/
|
||||
.. _OpenSSL: http://www.openssl.org
|
|
@ -0,0 +1,5 @@
|
|||
#! /bin/sh
|
||||
|
||||
size=$(identify $1 | cut -d ' ' -f 3)
|
||||
composite $1 -size $(identify $1 | cut -d ' ' -f3) xc:white $2
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
#! /usr/bin/python
|
||||
|
||||
"""A minimal reST frontend, to create appropriate LaTeX files."""
|
||||
|
||||
try:
|
||||
import locale
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
except:
|
||||
pass
|
||||
|
||||
from docutils.core import publish_cmdline, Publisher
|
||||
|
||||
def set_io(self, source_path=None, destination_path=None):
|
||||
Publisher.set_io_orig(self, source_path, destination_path='/dev/null')
|
||||
|
||||
Publisher.set_io_orig, Publisher.set_io = Publisher.set_io, set_io
|
||||
|
||||
output = publish_cmdline(writer_name='latex',
|
||||
settings_overrides = {
|
||||
'documentclass': 'report',
|
||||
'documentoptions': '11pt,a4paper,titlepage',
|
||||
'use_latex_toc': True,
|
||||
'use_latex_docinfo': True,
|
||||
'stylesheet': 'custom.tex'})
|
||||
|
||||
output = output.replace('\\includegraphics',
|
||||
'\\includegraphics[width=.9\\textwidth,height=15cm,clip,keepaspectratio]')
|
||||
output = output.replace('\\begin{figure}[htbp]', '\\begin{figure}[H]')
|
||||
print output
|
17
getlasso3.sh
|
@ -1,17 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Get venv site-packages path
|
||||
DSTDIR=`python3 -c 'import sysconfig; print(sysconfig.get_path("platlib"))'`
|
||||
|
||||
# Clean up
|
||||
rm -f $DSTDIR/lasso.*
|
||||
rm -f $DSTDIR/_lasso.*
|
||||
|
||||
# Link
|
||||
ln -sv /usr/lib/python3/dist-packages/lasso.py $DSTDIR/
|
||||
for SOFILE in /usr/lib/python3/dist-packages/_lasso.cpython-*.so
|
||||
do
|
||||
ln -sv $SOFILE $DSTDIR/
|
||||
done
|
||||
|
||||
exit 0
|
|
@ -0,0 +1,225 @@
|
|||
<page xmlns="http://projectmallard.org/1.0/"
|
||||
type="topic" id="api-auth" xml:lang="fr">
|
||||
|
||||
<info>
|
||||
<link type="guide" xref="index#api" />
|
||||
<revision docversion="0.1" date="2013-01-04" status="draft"/>
|
||||
<revision docversion="0.2" date="2015-12-18" status="draft"/>
|
||||
<credit type="author">
|
||||
<name>Frédéric Péters</name>
|
||||
<email>fpeters@entrouvert.com</email>
|
||||
</credit>
|
||||
<desc>Clé d'utilisation, utilisateurs, sessions, signatures, etc.</desc>
|
||||
|
||||
</info>
|
||||
|
||||
<title>Authentification</title>
|
||||
|
||||
<section>
|
||||
<title>Usager concerné</title>
|
||||
|
||||
<p>
|
||||
Pour les appels concernant un usager particulier (tel que la récupération de la
|
||||
liste de ses formulaires en cours), l'usager est précisé en ajoutant une query
|
||||
string avec un paramètre <code>email</code> (pour trouver l'usager selon son
|
||||
adresse électronique) ou un paramètre <code>NameID</code> (pour trouver
|
||||
l'usager selon son NameID SAML).
|
||||
</p>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="req-security-shared-secret">
|
||||
<title>Signature des requêtes</title>
|
||||
|
||||
<p>
|
||||
Les appels aux API doivent être signés, cela passe par une clé partagée à
|
||||
configurer des deux cotés de la liaison, la signature est du type HMAC;
|
||||
l'algorithme de hash à employer est passé en paramètre.
|
||||
</p>
|
||||
|
||||
<note><p>En ce qui concerne l'algorithme de hash, il est préconisé d'utiliser
|
||||
SHA-256 par respect du <link
|
||||
href="http://references.modernisation.gouv.fr/securite">Référentiel Général
|
||||
de Sécurité (RGS)</link>.</p></note>
|
||||
|
||||
<p>
|
||||
La signature est à calculer sur la query string encodée complète, en
|
||||
enlevant les paramètres terminaux <code>algo</code>, <code>timestamp</code>,
|
||||
<code>orig</code> et <code>signature</code>. La formule de calcul de la
|
||||
signature est la suivante :
|
||||
</p>
|
||||
|
||||
<code>
|
||||
BASE64(HMAC-HASH(query_string+'algo=HASH&timestamp=' + timestamp + '&orig=" + orig, clé))
|
||||
</code>
|
||||
|
||||
<list>
|
||||
|
||||
<item><p><code>timestamp</code> est la date dans la zone GMT au format ISO8601
|
||||
en se limitant à la précision des secondes (ex : 2012-04-04T12:34:00Z),
|
||||
</p></item>
|
||||
|
||||
<item><p><code>orig</code> est une chaîne précisant l'émetteur de la
|
||||
requête,</p></item>
|
||||
|
||||
<item><p>algo est une chaîne représentant l'algorithme de hachage utilisé, sont
|
||||
définis : sha1, sha256, sha512 pour les trois algorithmes correspondant.
|
||||
L'utilisation d'une valeur différente n'est pas définie.</p></item>
|
||||
|
||||
</list>
|
||||
|
||||
<p>
|
||||
La query string définitive est ainsi :
|
||||
</p>
|
||||
|
||||
<code>
|
||||
<var>qs_initial</var>&algo=<var>algo</var>&timestamp=<var>ts</var>&orig=<var>orig</var>&signature=<var>signature</var>
|
||||
</code>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<title>Configuration des clés partagées</title>
|
||||
|
||||
<p>
|
||||
Les clés partagées doivent être définies dans le fichier
|
||||
<code>site-options.cfg</code>, dans une section <code>[api-secrets]</code>, par
|
||||
exemple :
|
||||
</p>
|
||||
|
||||
<code>
|
||||
[api-secrets]
|
||||
intranet = 12345
|
||||
</code>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<title>Exemples de code de signature</title>
|
||||
|
||||
<p>
|
||||
Voici des exemples de code pour créer des URLs signées selon l'algorithme
|
||||
expliqué ci-dessus.
|
||||
</p>
|
||||
|
||||
<listing>
|
||||
<title>Python</title>
|
||||
<code mime="text/x-python">
|
||||
#!/usr/bin/env python2
|
||||
|
||||
import base64
|
||||
import hmac
|
||||
import hashlib
|
||||
import datetime
|
||||
import urllib
|
||||
import urlparse
|
||||
import random
|
||||
|
||||
def sign_url(url, key, algo='sha256', orig=None, timestamp=None, nonce=None):
|
||||
parsed = urlparse.urlparse(url)
|
||||
new_query = sign_query(parsed.query, key, algo, orig, timestamp, nonce)
|
||||
return urlparse.urlunparse(parsed[:4] + (new_query,) + parsed[5:])
|
||||
|
||||
def sign_query(query, key, algo='sha256', orig=None, timestamp=None, nonce=None):
|
||||
if timestamp is None:
|
||||
timestamp = datetime.datetime.utcnow()
|
||||
timestamp = timestamp.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
if nonce is None:
|
||||
nonce = hex(random.getrandbits(128))[2:-1]
|
||||
new_query = query
|
||||
if new_query:
|
||||
new_query += '&'
|
||||
new_query += urllib.urlencode((
|
||||
('algo', algo),
|
||||
('timestamp', timestamp),
|
||||
('nonce', nonce)))
|
||||
if orig is not None:
|
||||
new_query += '&' + urllib.urlencode({'orig': orig})
|
||||
signature = base64.b64encode(sign_string(new_query, key, algo=algo))
|
||||
new_query += '&' + urllib.urlencode({'signature':signature})
|
||||
return new_query
|
||||
|
||||
def sign_string(s, key, algo='sha256', timedelta=30):
|
||||
digestmod = getattr(hashlib, algo)
|
||||
hash = hmac.HMAC(key, digestmod=digestmod, msg=s)
|
||||
return hash.digest()
|
||||
|
||||
# usage:
|
||||
url = sign_url('http://www.example.net/uri/?arg=val&arg2=val2', 'user-key', orig='user')
|
||||
</code>
|
||||
</listing>
|
||||
|
||||
<listing>
|
||||
<title>PHP</title>
|
||||
<code mime="application/x-php">
|
||||
<?php
|
||||
|
||||
function sign_url(string $url, string $orig, string $key) {
|
||||
$parsed_url = parse_url($url);
|
||||
$timestamp = gmstrftime("%Y-%m-%dT%H:%M:%SZ");
|
||||
$nonce = bin2hex(random_bytes(16));
|
||||
$new_query = '';
|
||||
if (isset($parsed_url['query'])) {
|
||||
$new_query .= $parsed_url['query'] . '&';
|
||||
}
|
||||
$new_query .= http_build_query(array(
|
||||
'algo' => 'sha256',
|
||||
'timestamp' => $timestamp,
|
||||
'nonce' => $nonce,
|
||||
'orig' => $orig));
|
||||
$signature = base64_encode(hash_hmac('sha256', $new_query, $key, $raw_output = true));
|
||||
$new_query .= '&' . http_build_query(array('signature' => $signature));
|
||||
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
|
||||
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
|
||||
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
|
||||
$user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
|
||||
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
|
||||
$pass = ($user || $pass) ? "$pass@" : '';
|
||||
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
|
||||
$fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
|
||||
return "$scheme$user$pass$host$port$path?$new_query$fragment";
|
||||
}
|
||||
|
||||
# usage:
|
||||
url = sign_url("http://www.example.net/uri/?arg=val&arg2=val2", "user", "user-key");
|
||||
|
||||
?>
|
||||
</code>
|
||||
</listing>
|
||||
|
||||
<listing>
|
||||
<title>Shell (bash)</title>
|
||||
<code mime="application/x-shellscript">
|
||||
#!/bin/bash
|
||||
|
||||
url="http://www.example.net/uri/?arg=val&arg2=val2"
|
||||
orig="user"
|
||||
key="user-key"
|
||||
|
||||
function rawurlencode() {
|
||||
local string="${1}"
|
||||
local strlen=${#string}
|
||||
local encoded=""
|
||||
local pos c o
|
||||
for ((pos=0; pos<strlen; pos++)); do
|
||||
c=${string:$pos:1}
|
||||
case "$c" in
|
||||
[-_.~a-zA-Z0-9] ) o="${c}" ;;
|
||||
* ) printf -v o '%%%02x' "'$c"
|
||||
esac
|
||||
encoded+="${o}"
|
||||
done
|
||||
echo "${encoded}"
|
||||
}
|
||||
|
||||
now=$(date -u +%FT%TZ);
|
||||
qs="algo=sha256&timestamp=$now&orig=$orig"
|
||||
sig=$(rawurlencode $(echo -n "$qs" | openssl dgst -binary -sha256 -hmac "$key" | base64))
|
||||
signed="${url}?$qs&signature=$sig"
|
||||
echo "$signed"
|
||||
</code>
|
||||
</listing>
|
||||
|
||||
</section>
|
||||
|
||||
</page>
|
|
@ -1,455 +0,0 @@
|
|||
<page xmlns="http://projectmallard.org/1.0/"
|
||||
type="topic" id="api-cards" xml:lang="fr">
|
||||
|
||||
<info>
|
||||
<link type="guide" xref="index#api" />
|
||||
<revision docversion="0.1" date="2020-12-06" status="draft"/>
|
||||
<credit type="author">
|
||||
<name>Frédéric Péters</name>
|
||||
<email>fpeters@entrouvert.com</email>
|
||||
</credit>
|
||||
<desc>Liste de fiches, schémas de données, etc.</desc>
|
||||
|
||||
</info>
|
||||
|
||||
<title>Gestion des fiches</title>
|
||||
|
||||
<p>
|
||||
Une application tierce peut créer des fiches, récupérer et modifier les données
|
||||
des fiches, et peut également obtenir la liste des modèles de fiche et les
|
||||
schémas de données associés.
|
||||
</p>
|
||||
|
||||
<section id="create">
|
||||
<title>Création d’une fiche</title>
|
||||
|
||||
<p>
|
||||
La création d’une fiche se fait par une requête <code>POST</code> à
|
||||
l’adresse <code>/api/cards/<var>slug</var>/submit</code>, le contenu de
|
||||
la requête doit être un dictionnaire contenant obligatoirement un attribut
|
||||
<code>data</code>.
|
||||
</p>
|
||||
|
||||
<note>
|
||||
<p>
|
||||
Le <em>slug</em> est l’identifiant non-numérique utilisé dans les URL, il
|
||||
est visible depuis l’écran d’un modèle de fiche, dans la fenêtre de
|
||||
modification du titre.
|
||||
</p>
|
||||
</note>
|
||||
|
||||
<p>
|
||||
L’attribut <code>data</code> est obligatoire et contient un dictionnaire
|
||||
dont les clés sont les noms de variable (remplacé ici par
|
||||
<var>varname</var>) des champs de la fiche et les valeurs le contenu de
|
||||
ces champs.
|
||||
</p>
|
||||
|
||||
<list>
|
||||
<item>
|
||||
<p>
|
||||
Les champs de type simple tels que « Texte », « Texte long » ou
|
||||
« Courriel » sont des chaînes de caractères.
|
||||
</p>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<p>
|
||||
Les champs de type « Liste » et « Liste à choix multiples » acceptent
|
||||
différentes valeurs selon leur configuration, ceci est décrit dans
|
||||
<link xref="api-fill#fill-list"/>.
|
||||
</p>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<p>
|
||||
Les champs de type « Date » sont des chaînes de caractères au format
|
||||
ISO-8601, i.e. <code>YYYY-MM-DD</code>.
|
||||
</p>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<p>
|
||||
Les champs de type « Fichier » sont des dictionnaires contenant les clés
|
||||
<code>filename</code> pour le nom de fichier et <code>content</code> pour le
|
||||
contenu de celui-ci, encodé en base64.
|
||||
</p>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<p>
|
||||
Les champs de type « Carte » sont des dictionnaires contenant les clés
|
||||
<code>lat</code> pour la latitute en nombre décimal et <code>lon</code>
|
||||
pour la longitude en nombre décimal.
|
||||
</p>
|
||||
</item>
|
||||
</list>
|
||||
|
||||
<p>
|
||||
L’exemple suivant crée une fiche « Parking », dont le modèle
|
||||
de fiche a comme identifiant « parkings », qui demanderait trois champs :
|
||||
adresse (nom de variable <code>adresse</code>), date d’ouverture
|
||||
(nom de variable <code>date_ouverture</code>) et nom (nom de variable
|
||||
<code>nom</code>).
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>POST https://www.example.net/api/cards/parkings/submit</input>
|
||||
<output>{"err": 0, "data": {"id": "5"}}</output>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
Avec les données suivantes en entrée :
|
||||
</p>
|
||||
|
||||
<code mime="application/json">
|
||||
{
|
||||
"data": {
|
||||
"adresse": "rue de l’Opéra",
|
||||
"date_ouverture": "2020-11-12",
|
||||
"nom": "Parking Opéra-Tolozan"
|
||||
}
|
||||
}
|
||||
</code>
|
||||
|
||||
<note>
|
||||
<p>
|
||||
Il n’y a aucune vérification du format des données reçues, elles sont
|
||||
enregistrée telles quelles. Les contraintes de validation ou les conditions
|
||||
d’affichage ne sont pas prises en compte.
|
||||
</p>
|
||||
</note>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="card-import-csv">
|
||||
<title>Création d’un ensemble de fiches par import CSV</title>
|
||||
|
||||
<p>
|
||||
Il est possible de créer un ensemble de fiches par import d’un fichier CSV.
|
||||
Cela s’effectue par une requête <code>PUT</code> à l’adresse
|
||||
<code>/api/cards/<var>slug</var>/import-csv</code>. Le contenu de la requête
|
||||
doit être un fichier CSV (text/csv).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Chaque ligne du fichier va provoquer la création d’une nouvelle fiche et lancer
|
||||
le workflow correspondant.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>PUT https://www.example.net/api/cards/<var>slug</var>/import-csv</input>
|
||||
<output>{"err": 0}</output>
|
||||
</screen>
|
||||
|
||||
<p>Le fichier CSV doit suivre le même format que celui utilisé lors d’un import
|
||||
CSV dans l’interface de gestion.</p>
|
||||
|
||||
<section id="card-import-csv-async">
|
||||
<title>Import CSV asynchrone (recommandé)</title>
|
||||
|
||||
<p>
|
||||
En plus de la création des fiches, le workflow va être exécuté pour chacune :
|
||||
sur un fichier CSV important le temps d’exécution de l’import peut dépasser la
|
||||
limite acceptée par le serveur HTTP (souvent 20 ou 30 secondes). Il est donc
|
||||
recommandé d’utiliser l’option asynchrone de l’import CSV.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Pour faire un import asynchrone, ajouter <code>async=on</code> dans les
|
||||
paramètres de l’URL :
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>PUT https://www.example.net/api/cards/<var>slug</var>/import-csv<var>?async=on</var></input>
|
||||
<output>{
|
||||
"err": 0,
|
||||
"data": {
|
||||
"job": {
|
||||
"id": "1234",
|
||||
"url": "https://www.example.net/api/jobs/1234/"
|
||||
}
|
||||
}
|
||||
}</output>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
Cet appel envoie le fichier CSV, mais il n’est pas aussitôt importé. Une tâche
|
||||
(<em>job</em>) est lancée qui va effectivement faire l’import, et on peut en
|
||||
suivre la progression en appellant son URL indiquée en retour de l’appel PUT.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/jobs/1234/</input>
|
||||
<output>{
|
||||
"err": 0,
|
||||
"data": {
|
||||
"status": "en cours",
|
||||
"label": "Importation des données dans des fiches",
|
||||
"creation_time": 1634910701,
|
||||
"completion_time": null,
|
||||
"completion_status": "23/46 (50%)"
|
||||
}
|
||||
}</output>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
Pour suivre la bonne exécution de l’import, il faut appeler cette URL jusqu’à
|
||||
ce que la valeur <code>completion_time</code> soit renseignée. La valeur
|
||||
<code>status</code> permet de savoir alors si l’import s'est correctement
|
||||
déroulé.
|
||||
</p>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="card">
|
||||
<title>Récupération des données d’une fiche</title>
|
||||
|
||||
<p>
|
||||
L’exemple suivant récupère les données d’une fiche « Parking », dont le modèle
|
||||
de fiche a comme identifiant « parkings ».
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/cards/parkings/5/</input>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
Le contenu ainsi obtenu est le suivant :
|
||||
</p>
|
||||
|
||||
<code mime="application/json">
|
||||
{
|
||||
"digest" : "Parking Opéra-Tolozan",
|
||||
"display_id" : "31-5",
|
||||
"display_name" : "Parkings - n°31-5",
|
||||
"id" : "5",
|
||||
"last_update_time" : "2020-11-24T14:18:16",
|
||||
"receipt_time" : "2020-11-06T14:48:07",
|
||||
"fields" : {
|
||||
"adresse" : "rue de l’Opéra",
|
||||
"date_ouverture" : "2020-11-12",
|
||||
"nom" : "Parking Opéra-Tolozan"
|
||||
},
|
||||
"text" : "Parking Opéra-Tolozan",
|
||||
"url" : "https://.../backoffice/data/parkings/5/",
|
||||
"workflow" : {
|
||||
"status" : {
|
||||
"id" : "recorded",
|
||||
"name" : "Recorded",
|
||||
"endpoint": false
|
||||
}
|
||||
},
|
||||
"evolution" : [...],
|
||||
"geolocations": {...},
|
||||
"roles": {...}
|
||||
}
|
||||
</code>
|
||||
|
||||
<p>
|
||||
La structure du contenu correspond à celle de l’API de <link xref="#create"/>.
|
||||
</p>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="card-edit">
|
||||
<title>Modification des données d’une fiche</title>
|
||||
|
||||
<p>
|
||||
Sur le même modèle que les formulaires une fiche qui peut être modifiée (par
|
||||
la présence d’une action de workflow de type « Édition ») peut également être
|
||||
modifiée via un appel à l’API.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Les données attendues sont similaires à la création d’une nouvelle fiche
|
||||
(<link xref="#create"/>), seuls les champs présents seront pris en compte.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>POST https://www.example.net/api/cards/parkings/5/</input>
|
||||
</screen>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<section id="listing">
|
||||
<title>Liste de fiches</title>
|
||||
|
||||
<p>
|
||||
La liste des fiches d’un modèle donné est destinée à être utilisée par
|
||||
un système de synchronisation. Elle ne retourne donc pour chaque fiche que
|
||||
son numéro (id), ses dates de création et de dernière mise à jour.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Un système de synchronisation vérifiera depuis cette liste si de nouvelles
|
||||
demandes existent, ou si certaines ont été mises à jour, sont obsolètes ou
|
||||
effacées, puis effectuera pour chacune les actions nécessaires.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/cards/parkings/list</input>
|
||||
<output>{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"text": "Parking de la place",
|
||||
"url": "https://www.example.net/backoffice/data/parkings/1/",
|
||||
"last_update_time": "2015-03-26T23:08:45",
|
||||
"receipt_time": "2015-03-26T23:08:44",
|
||||
"display_id": "12-1",
|
||||
"display_name": "Parkings - n°12-1"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"text": "Parking des nénuphars",
|
||||
"url": "https://www.example.net/backoffice/data/parkings/2/",
|
||||
"last_update_time": "2015-03-27T09:03:12",
|
||||
"receipt_time": "2015-03-27T09:03:12",
|
||||
"display_id": "12-2",
|
||||
"display_name": "Parkings - n°12-2"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"text": "Parking de la rivière",
|
||||
"url": "https://www.example.net/backoffice/data/parkings/3/",
|
||||
"last_update_time": "2015-03-27T12:11:21",
|
||||
"receipt_time": "2015-03-27T12:45:19",
|
||||
"display_id": "12-3",
|
||||
"display_name": "Parkings - n°12-3"
|
||||
}
|
||||
]
|
||||
}</output>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
Des paramètres peuvent être envoyés dans la requête pour filtrer la liste des
|
||||
fiches, ils sont similaires à ceux de l’API de <link
|
||||
xref="api-get#listing">récupération d’une liste de formulaires</link>. Les
|
||||
autres paramètres de cette API sont également exploitables, pour inclure
|
||||
l’ensemble des données (<code>full=on</code>) ou anonymiser celles-ci
|
||||
(<code>anonymise</code>).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Il est également possible de récupérer une liste filtrée correspondant à une
|
||||
vue personnalisée, en ajoutant l’identifiant de celle-ci à l’adresse, ex :
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/cards/parkings/list/vue-personnalisee</input>
|
||||
<output>{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"text": "Parking de la place",
|
||||
"url": "https://www.example.net/backoffice/data/parkings/1/",
|
||||
"last_update_time": "2015-03-26T23:08:45",
|
||||
"receipt_time": "2015-03-26T23:08:44",
|
||||
"display_id": "12-1",
|
||||
"display_name": "Parkings - n°12-1"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"text": "Parking de la rivière",
|
||||
"url": "https://www.example.net/backoffice/data/parkings/3/",
|
||||
"last_update_time": "2015-03-27T12:11:21",
|
||||
"receipt_time": "2015-03-27T12:45:19",
|
||||
"display_id": "12-3",
|
||||
"display_name": "Parkings - n°12-3"
|
||||
}
|
||||
]
|
||||
}</output>
|
||||
</screen>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="card-schema">
|
||||
<title>Schéma de données</title>
|
||||
|
||||
<p>
|
||||
Une API existe pour récupérer le schéma de données d’un modèle de fiches.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/cards/parkings/@schema</input>
|
||||
<output>{
|
||||
"always_advertise" : false,
|
||||
"appearance_keywords" : null,
|
||||
"confirmation" : false,
|
||||
"description" : null,
|
||||
"detailed_emails" : true,
|
||||
"digest_template" : "{{form_var_nom}}",
|
||||
"disabled" : false,
|
||||
"disabled_redirection" : null,
|
||||
"discussion" : false,
|
||||
"drafts_lifespan" : null,
|
||||
"drafts_max_per_user" : null,
|
||||
"enable_tracking_codes" : false,
|
||||
"expiration_date" : null,
|
||||
"fields" : [
|
||||
{
|
||||
"anonymise" : true,
|
||||
"data_source" : {},
|
||||
"label" : "Nom",
|
||||
"prefill" : {
|
||||
"type" : "none"
|
||||
},
|
||||
"required" : true,
|
||||
"type" : "string",
|
||||
"validation" : {},
|
||||
"varname" : "nom"
|
||||
},
|
||||
...
|
||||
],
|
||||
"workflow" : {
|
||||
"fields" : [],
|
||||
"functions" : {
|
||||
"_editor" : "Editor",
|
||||
"_viewer" : "Viewer"
|
||||
},
|
||||
"name" : "Fiche parking",
|
||||
"statuses" : [
|
||||
{
|
||||
"endpoint" : false,
|
||||
"forced_endpoint" : false,
|
||||
"id" : "recorded",
|
||||
"name" : "Recorded",
|
||||
"waitpoint" : true
|
||||
},
|
||||
...
|
||||
}
|
||||
}</output>
|
||||
</screen>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="card-models">
|
||||
<title>Liste des modèles de fiches</title>
|
||||
|
||||
<p>Une API permet de récupérer la liste des modèles de fiches.</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/cards/@list</input>
|
||||
<output>{
|
||||
"data" : [
|
||||
{
|
||||
"custom_views" : [],
|
||||
"description" : "",
|
||||
"id" : "parkings",
|
||||
"keywords" : [],
|
||||
"slug" : "parkings",
|
||||
"text" : "Parkings",
|
||||
"title" : "Parkings",
|
||||
"url" : "https://.../backoffice/data/parkings/"
|
||||
},
|
||||
...
|
||||
}
|
||||
}</output>
|
||||
</screen>
|
||||
|
||||
</section>
|
||||
|
||||
</page>
|
|
@ -18,7 +18,7 @@
|
|||
w.c.s. peut utiliser des référentiels externes pour par exemple alimenter la
|
||||
liste des choix possibles dans un champ; pour ce faire w.c.s. utilise le
|
||||
format JSON.
|
||||
L’adresse appelée doit répondre aux exigences suivantes :
|
||||
L'adresse appelée doit répondre aux exigences suivantes :
|
||||
</p>
|
||||
|
||||
<list>
|
||||
|
@ -32,7 +32,7 @@ L’adresse appelée doit répondre aux exigences suivantes :
|
|||
<example>
|
||||
<title>Exemple JSON</title>
|
||||
<screen>
|
||||
<input>GET https://www.example.net/data/fruits</input>
|
||||
<output style="prompt">$ </output><input>curl https://www.example.net/data/fruits</input>
|
||||
<output>{
|
||||
"data": [
|
||||
{
|
||||
|
@ -49,7 +49,7 @@ L’adresse appelée doit répondre aux exigences suivantes :
|
|||
|
||||
<p>
|
||||
Quand il y a besoin de filtrer dynamiquement les données
|
||||
(autocomplétion, recherche dans un champ liste), l’adresse appellée
|
||||
(autocomplétion, recherche dans un champ liste), l'adresse appellée
|
||||
doit respecter les exigences supplémentaires suivantes :
|
||||
</p>
|
||||
|
||||
|
@ -61,9 +61,9 @@ doit respecter les exigences supplémentaires suivantes :
|
|||
</list>
|
||||
|
||||
<example>
|
||||
<title>Exemple JSON d’un élément unique désigné par son identifiant</title>
|
||||
<title>Exemple JSON d'un élément unique désigné par son identifiant</title>
|
||||
<screen>
|
||||
<input>GET https://www.example.net/data/fruits?id=1</input>
|
||||
<output style="prompt">$ </output><input>curl https://www.example.net/data/fruits?id=1</input>
|
||||
<output>{
|
||||
"data": [
|
||||
{
|
||||
|
@ -77,7 +77,7 @@ doit respecter les exigences supplémentaires suivantes :
|
|||
<example>
|
||||
<title>Exemple JSON filtré par contenu</title>
|
||||
<screen>
|
||||
<input>GET https://www.example.net/data/fruits?q=pom</input>
|
||||
<output style="prompt">$ </output><input>curl https://www.example.net/data/fruits?q=pom</input>
|
||||
<output>{
|
||||
"data": [
|
||||
{
|
||||
|
@ -98,7 +98,7 @@ de contexte du formulaire.
|
|||
<example>
|
||||
<title>Exemple JSON enrichi</title>
|
||||
<screen>
|
||||
<input>GET https://www.example.net/data/fruits</input>
|
||||
<output style="prompt">$ </output><input>curl https://www.example.net/data/fruits</input>
|
||||
<output>{
|
||||
"data": [
|
||||
{
|
||||
|
|
|
@ -12,43 +12,27 @@
|
|||
|
||||
</info>
|
||||
|
||||
<title>Complétion et modification d’un formulaire</title>
|
||||
<title>Complétion et modification d'un formulaire</title>
|
||||
|
||||
<p>
|
||||
w.c.s expose une API autorisant les logiciels tiers à transmettre des données
|
||||
structurées permettant la complétion d’un formulaire ou la modification d’un
|
||||
structurées permettant la complétion d'un formulaire ou la modification d'un
|
||||
formulaire existant.
|
||||
</p>
|
||||
|
||||
<note>
|
||||
<p>
|
||||
Il n’y a aucune vérification du format des données reçues, elles sont
|
||||
enregistrée telles quelles. Les contraintes de validation ou les conditions
|
||||
d’affichage ne sont pas prises en compte.
|
||||
</p>
|
||||
</note>
|
||||
|
||||
<section id="create">
|
||||
<title>Complétion d’un formulaire</title>
|
||||
<title>Complétion d'un formulaire</title>
|
||||
|
||||
<p>
|
||||
La complétion d’un formulaire se fait par une requête <code>POST</code> à
|
||||
l’adresse <code>/api/formdefs/<var>slug</var>/submit</code>, le contenu de
|
||||
La complétion d'un formulaire se fait par une requête <code>POST</code> à
|
||||
l'adresse <code>/api/formdefs/<var>slug</var>/submit</code>, le contenu de
|
||||
la requête doit être un dictionnaire contenant obligatoirement un attribut
|
||||
<code>data</code> et optionnellement un attribut <code>meta</code> et un
|
||||
attribut <code>context</code>.
|
||||
</p>
|
||||
|
||||
<note>
|
||||
<p>
|
||||
Le <em>slug</em> est l’identifiant non-numérique utilisé dans les URL, il
|
||||
est visible depuis l’écran d’un formulaire, dans la fenêtre de modification
|
||||
du titre.
|
||||
</p>
|
||||
</note>
|
||||
|
||||
<p>
|
||||
L’attribut <code>data</code> est obligatoire et contient un dictionnaire
|
||||
L'attribut <code>data</code> est obligatoire et contient un dictionnaire
|
||||
dont les clés sont les noms de variable (remplacé ici par
|
||||
<var>varname</var>) des champs du formulaire et les valeurs le contenu de
|
||||
ces champs.
|
||||
|
@ -95,15 +79,7 @@ formulaire existant.
|
|||
</list>
|
||||
|
||||
<p>
|
||||
L’attribut <code>user</code> est optionnel et peut contenir un identifiant
|
||||
permettant d’associer la demande à un usager existant; l’identifiant peut
|
||||
être soit l’identifiant unique (UUID/NameID), passé dans une clé
|
||||
<code>NameID</code>, soit l’adresse électronique de l’usager, passée dans
|
||||
une clé <code>email</code>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
L’attribut <code>meta</code> est optionnel et contient une série de
|
||||
L'attribut <code>meta</code> est optionnel et contient une série de
|
||||
paramètres supplémentaires concernant le formulaire.
|
||||
</p>
|
||||
|
||||
|
@ -122,26 +98,30 @@ formulaire existant.
|
|||
</table>
|
||||
|
||||
<p>
|
||||
L’attribut <code>context</code> est également optionnel et contient une
|
||||
série de renseignements supplémentaires sur le contexte de l’envoi du
|
||||
L'attribut <code>context</code> est également optionnel et contient une
|
||||
série de renseignements supplémentaires sur le contexte de l'envoi du
|
||||
formulaire. Les attributs reconnus sont <code>channel</code>,
|
||||
<code>thumbnail_url</code>, <code>user_id</code> et <code>comments</code>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
L’exemple suivant complète un formulaire d’inscription à une newsletter, qui
|
||||
L'exemple suivant complète un formulaire d'inscription à une newsletter, qui
|
||||
demanderait trois champs : prénom (nom de variable <code>prenom</code>), nom
|
||||
(nom de variable <code>nom</code>) et adresse électronique (nom de variable
|
||||
<code>email</code>).
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>POST https://www.example.net/api/formdefs/newsletter/submit</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Content-type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d@donnees.json \
|
||||
https://www.example.net/api/formdefs/newsletter/submit<var>?signature…</var></input>
|
||||
<output>{"err": 0, "data": {"id": "1"}}</output>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
Avec les données suivantes en entrée :
|
||||
Le fichier de données utilisé (<file>donnees.json</file>) contient le
|
||||
dictionnaire JSON suivant :
|
||||
</p>
|
||||
|
||||
<code mime="application/json">
|
||||
|
@ -150,9 +130,6 @@ formulaire existant.
|
|||
"prenom": "Marc",
|
||||
"nom": "L.",
|
||||
"email": "marc@example.net"
|
||||
},
|
||||
"user": {
|
||||
"email": "marc@example.net"
|
||||
}
|
||||
}
|
||||
</code>
|
||||
|
@ -160,16 +137,16 @@ formulaire existant.
|
|||
</section>
|
||||
|
||||
<section id="edit">
|
||||
<title>Modification d’un formulaire</title>
|
||||
<title>Modification d'un formulaire</title>
|
||||
|
||||
<p>
|
||||
Un formulaire qui peut être modifié (par la présence d’une action de workflow
|
||||
de type « Édition ») peut également être modifié via un appel à
|
||||
l’API, en faisant un <code>POST</code> sur l’adresse du formulaire.
|
||||
Un formulaire qui peut être modifié (par la présence d'une action de workflow
|
||||
de type « Permettre l'édition ») peut également être modifié via un appel à
|
||||
l'API, en faisant un <code>POST</code> sur l'adresse du formulaire.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Les données attendues sont similaires à la création d’un nouveau formulaire,
|
||||
Les données attendues sont similaires à la création d'un nouveau formulaire,
|
||||
seuls les champs présents seront pris en compte.
|
||||
</p>
|
||||
|
||||
|
@ -178,7 +155,10 @@ formulaire existant.
|
|||
</p>
|
||||
|
||||
<screen>
|
||||
<input>POST https://www.example.net/api/forms/newsletter/1/</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Content-type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d@donnees.json \
|
||||
https://www.example.net/api/forms/newsletter/1/<var>?signature…</var></input>
|
||||
<output>{"err": 0}</output>
|
||||
</screen>
|
||||
|
||||
|
@ -202,7 +182,7 @@ formulaire existant.
|
|||
|
||||
<p>
|
||||
Pour les champs de type « Liste », si le champ est configuré avec une simple
|
||||
liste d’options, la valeur doit être une chaîne tirée de la liste.
|
||||
liste d'options, la valeur doit être une chaîne tirée de la liste.
|
||||
</p>
|
||||
|
||||
<listing>
|
||||
|
@ -215,15 +195,15 @@ formulaire existant.
|
|||
|
||||
<p>
|
||||
Si le champ est configuré pour tirer ses options depuis une source de
|
||||
données, la valeur peut être l’identifiant d’une donnée structurée ou si la
|
||||
donnée structurée complète est transmise, l’identifiant de la donnée dans
|
||||
données, la valeur peut être l'identifiant d'une donnée structurée ou si la
|
||||
donnée structurée complète est transmise, l'identifiant de la donnée dans
|
||||
une clé suffixée de <code>_raw</code>, le libellé de la donnée dans la clé
|
||||
normale et éventuellement la donnée structurée complète dans une clé
|
||||
suffixée de <code>_structured</code>.
|
||||
</p>
|
||||
|
||||
<listing>
|
||||
<title>Identifiant d’une option</title>
|
||||
<title>Identifiant d'une option</title>
|
||||
<code>
|
||||
"data": {
|
||||
"<var>varname</var>": "1"
|
||||
|
@ -249,7 +229,7 @@ formulaire existant.
|
|||
|
||||
<p>
|
||||
Pour les champs de type « Liste à choix multiple », si le champ est
|
||||
configuré avec une simple liste d’options, la valeur doit être une
|
||||
configuré avec une simple liste d'options, la valeur doit être une
|
||||
liste de chaînes tirées de la liste.
|
||||
</p>
|
||||
|
||||
|
@ -263,7 +243,7 @@ formulaire existant.
|
|||
|
||||
<p>
|
||||
Si le champ est configuré pour tirer ses options depuis une source de
|
||||
données, la valeur peut être une liste d’identifiants ou,
|
||||
données, la valeur peut être une liste d'identifiants ou,
|
||||
si la donnée structurée complète est transmise, la liste des identifiants
|
||||
de la donnée dans une clé suffixée de <code>_raw</code>, la liste des
|
||||
libellés de la donnée dans la clé normale et éventuellement la liste des
|
||||
|
@ -272,7 +252,7 @@ formulaire existant.
|
|||
</p>
|
||||
|
||||
<listing>
|
||||
<title>Liste d’identifiants d’options</title>
|
||||
<title>Liste d'identifiants d'options</title>
|
||||
<code>
|
||||
"data": {
|
||||
"<var>varname</var>": ["1", "2"]
|
||||
|
|
|
@ -12,25 +12,26 @@
|
|||
|
||||
</info>
|
||||
|
||||
<title>Récupération des données d’un formulaire</title>
|
||||
<title>Récupération des données d'un formulaire</title>
|
||||
|
||||
<p>
|
||||
Il s’agit ici d’une API permettant à un logiciel tiers de récupérer les données
|
||||
Il s'agit ici d'une API permettant à un logiciel tiers de récupérer les données
|
||||
associées à un formulaire complété; cet accès peut aussi bien être initié par
|
||||
l’application tierce (mode pull) ou par w.c.s., à différents moments du
|
||||
traitement d’un formulaire (mode push).
|
||||
l'application tierce (mode pull) ou par w.c.s., à différents moments du
|
||||
traitement d'un formulaire (mode push).
|
||||
</p>
|
||||
|
||||
<section id="pull">
|
||||
<title>Mode « Pull »</title>
|
||||
|
||||
<p>
|
||||
L’exemple suivant récupère les données d’un formulaire d’inscription à une
|
||||
L'exemple suivant récupère les données d'un formulaire d'inscription à une
|
||||
newsletter.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/forms/newsletter/16/</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/forms/newsletter/16/<var>?signature…</var></input>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
|
@ -60,8 +61,7 @@ Le contenu ainsi obtenu est le suivant :
|
|||
"workflow": {
|
||||
"status": {
|
||||
"id": "1",
|
||||
"name": "New",
|
||||
"endpoint": false
|
||||
"name": "New"
|
||||
},
|
||||
"data": {
|
||||
"creation_status": 200,
|
||||
|
@ -130,9 +130,9 @@ Le contenu ainsi obtenu est le suivant :
|
|||
"parts": [
|
||||
{
|
||||
"type": "wscall-error",
|
||||
"summary": "description de l’erreur",
|
||||
"summary": "description de l'erreur",
|
||||
"label": "appel du web-service XYZ",
|
||||
"data": "données reçues jusqu’à 10000 octets..."
|
||||
"data": "données reçues jusqu'à 10000 octets..."
|
||||
},
|
||||
{
|
||||
"type": "workflow-comment",
|
||||
|
@ -149,39 +149,40 @@ Seuls les champs ayant un <em>nom de variable</em> sont exportés dans <code>fie
|
|||
</p>
|
||||
|
||||
<p>
|
||||
Les différentes géolocalisation associées au formulaire sont listées dans l’attribut
|
||||
<code>geolocations</code>. Pour l’instant il n’en existe qu’une toujours nommée <code>base</code>.
|
||||
Les différentes géolocalisation associées au formulaire sont listées dans l'attribut
|
||||
<code>geolocations</code>. Pour l'instant il n'en existe qu'une toujours nommée <code>base</code>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Concernant les rôles et fonctions de workflow, les différents intervenants sont
|
||||
listés dans l’attribut <code>roles</code>, en différentes séries qui vont
|
||||
listés dans l'attribut <code>roles</code>, en différentes séries qui vont
|
||||
dépendre de fonctions attachées au workflow. Deux séries sont particulières,
|
||||
la série <code>concerned</code> reprend les rôles concernés par la demande et
|
||||
la série <code>actions</code> reprend les rôles disposant d’une capacité
|
||||
d’action sur la demande.
|
||||
la série <code>actions</code> reprend les rôles disposant d'une capacité
|
||||
d'action sur la demande.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
L’information sur l’origine de la demande, si la saisie a eu lieu depuis le
|
||||
backoffice et quel était le canal d’origine de la demande, est disponible
|
||||
dans l’attribut <code>submission</code>.
|
||||
L'information sur l'origine de la demande, si la saisie a eu lieu depuis le
|
||||
backoffice et quel était le canal d'origine de la demande, est disponible
|
||||
dans l'attribut <code>submission</code>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
L’historique du formulaire, ses transitions dans différents statuts, est disponible dans l’attribut
|
||||
<code>evolution</code>. Cette liste de dictionnaires contient l’instant de la transition
|
||||
dans l’attribut <code>time</code>, le code du statut concerné dans <code>status</code> et
|
||||
une description de l’utilisateur responsable de la transition dans <code>user</code>. L’attribut
|
||||
L'historique du formulaire, ses transitions dans différents statuts, est disponible dans l'attribut
|
||||
<code>evolution</code>. Cette liste de dictionnaires contient l'instant de la transition
|
||||
dans l'attribut <code>time</code>, le code du statut concerné dans <code>status</code> et
|
||||
une description de l'utilisateur responsable de la transition dans <code>user</code>. L'attribut
|
||||
optionnel <code>parts</code> peut contenir une liste de dictionnaires liés aux actions de workflow,
|
||||
comme un commentaire ou une erreur lors de l’appel d’un <em>web service</em>.
|
||||
comme un commentaire ou une erreur lors de l'appel d'un <em>web service</em>.
|
||||
</p>
|
||||
|
||||
|
||||
<note>
|
||||
<p>
|
||||
Il est bien sûr nécessaire de disposer des autorisations nécessaires pour
|
||||
accéder ainsi aux données d’un formulaire.
|
||||
accéder ainsi aux données d'un formulaire. (cf <link
|
||||
xref="api-auth"/> pour les explications sur le sujet)
|
||||
</p>
|
||||
</note>
|
||||
|
||||
|
@ -191,15 +192,15 @@ comme un commentaire ou une erreur lors de l’appel d’un <em>web service</em>
|
|||
<title>Mode « push »</title>
|
||||
|
||||
<p>
|
||||
Il est également possible pour un workflow d’être configuré pour transmettre
|
||||
Il est également possible pour un workflow d'être configuré pour transmettre
|
||||
les données à une URL fournie par une application tierce. Un document JSON
|
||||
(tel celui donné plus haut) est alors transmis en utilisant la méthode POST.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
En retour, l’application tierce peut fournir un objet JSON qui sera enregistré
|
||||
En retour, l'application tierce peut fournir un objet JSON qui sera enregistré
|
||||
dans les données du workflow du formulaire (voir le dictionnaire "data" dans
|
||||
l’exemple ci-dessus).
|
||||
l'exemple ci-dessus).
|
||||
</p>
|
||||
|
||||
</section>
|
||||
|
@ -209,7 +210,7 @@ l’exemple ci-dessus).
|
|||
<title>Types de données</title>
|
||||
|
||||
<p>
|
||||
Les données d’un formulaire sont placées dans le champs <code>fields</code> de
|
||||
Les données d'un formulaire sont placées dans le champs <code>fields</code> de
|
||||
la réponse. Les champs de type simple tels que « Texte », « Texte long » ou
|
||||
« Courriel » sont vus en tant que chaîne de caractères :
|
||||
</p>
|
||||
|
@ -225,7 +226,7 @@ la réponse. Les champs de type simple tels que « Texte », « Texte long
|
|||
</code>
|
||||
|
||||
<section>
|
||||
<title>Représentation d’un champ « Fichier »</title>
|
||||
<title>Représentation d'un champ « Fichier »</title>
|
||||
|
||||
<p>
|
||||
Les champs de type « Fichier » sont exportés selon le schéma suivant :
|
||||
|
@ -237,8 +238,7 @@ Les champs de type « Fichier » sont exportés selon le schéma suivant :
|
|||
"photo": {
|
||||
"filename": "exemple.txt",
|
||||
"content_type": "text/plain",
|
||||
"content": "Q2VjaSBuJ2VzdCBwYXMgdW4gZXhlbXBsZS4=",
|
||||
"url": "https://.../"
|
||||
"content": "Q2VjaSBuJ2VzdCBwYXMgdW4gZXhlbXBsZS4="
|
||||
}
|
||||
},
|
||||
(...)
|
||||
|
@ -255,6 +255,11 @@ La valeur de <code>content</code> est le contenu du fichier, encodé en base64.
|
|||
<section id="listing">
|
||||
<title>Liste de formulaires</title>
|
||||
|
||||
<note style="important"><p>
|
||||
Ce webservice n'est pas encore stabilisé, son URL peut encore changer dans les
|
||||
futures versions de w.c.s.
|
||||
</p></note>
|
||||
|
||||
<p>
|
||||
La liste des demandes pour un formulaire donné est destinée à être utilisée par
|
||||
un système de synchronisation. Elle ne retourne donc pour chaque demande que
|
||||
|
@ -269,35 +274,37 @@ etc.).
|
|||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/forms/inscriptions/list</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/forms/inscriptions/list<var>?signature…</var></input>
|
||||
</screen>
|
||||
|
||||
<code mime="application/json">
|
||||
[
|
||||
{
|
||||
"url": "https://www.example.net/inscriptions/1/",
|
||||
"last_update_time": "2015-03-26T23:08:45",
|
||||
"receipt_time": "2015-03-26T23:08:44",
|
||||
"id": 1
|
||||
url: "https://www.example.net/inscriptions/1/",
|
||||
last_update_time: "2015-03-26T23:08:45",
|
||||
receipt_time: "2015-03-26T23:08:44",
|
||||
id: 1
|
||||
},
|
||||
{
|
||||
"url": "https://www.example.net/inscriptions/3/",
|
||||
"last_update_time": "2015-03-27T12:11:21",
|
||||
"receipt_time": "2015-03-27T12:45:19",
|
||||
"id": 3
|
||||
url: "https://www.example.net/inscriptions/3/",
|
||||
last_update_time: "2015-03-27T12:11:21",
|
||||
receipt_time: "2015-03-27T12:45:19",
|
||||
id: 3
|
||||
}
|
||||
]
|
||||
</code>
|
||||
|
||||
<p>
|
||||
Des paramètres peuvent être envoyés dans la requête pour filtrer le listing
|
||||
voulu. Il s’agit des mêmes paramètres que pour l’export ou le listing en backoffice, sauf
|
||||
voulu. Il s'agit des mêmes paramètres que pour l'export ou le listing en backoffice, sauf
|
||||
pour filter qui est fixé à all par défaut. Pour avoir une liste limitée aux
|
||||
demandes non terminées (pending) :
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/forms/inscriptions/list?filter=pending</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/forms/inscriptions/list?filter=pending<var>&signature…</var></input>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
|
@ -308,133 +315,100 @@ possibles est « gratuit » :
|
|||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/forms/inscriptions/list?filter-type=gratuit</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/forms/inscriptions/list?filter-type=gratuit<var>&signature…</var></input>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
D’autres paramètres de filtres existent. Pour filtrer sur les demandes déposées
|
||||
après une date donnée
|
||||
(<code>?filter-start=on&filter-start-value=2020-01-03</code>),
|
||||
ou avant une date donnée
|
||||
(<code>?filter-end=on&filter-end-value=2020-01-03</code>) et de la même
|
||||
manière sur les demandes modifiées après ou avant une date,
|
||||
(<code>?filter-start-mtime=on&filter-start-mtime-value=2020-01-03</code>
|
||||
ou <code>?filter-end-mtime=on&filter-end-mtime-value=2020-01-03</code>).
|
||||
Pour filtrer selon l’usager associé (<code>?filter-user-uuid=XYZ</code>) ou
|
||||
selon l’appartenance d’un usager à une fonction particulière
|
||||
(<code>?filter-user-function=_mandataire&filter-user-uuid=XYZ</code>).
|
||||
Et pour filtrer selon l’agent qui a fait la saisie en backoffice
|
||||
(<code>?filter-submission-agent-uuid=XYZ</code>).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Afin de faciliter certains traitements <em>batch</em>, il est possible de
|
||||
demander que l’ensemble des données associées aux formulaires soient
|
||||
retourné, plutôt qu’un jeu réduit adapté aux systèmes de synchronisation.
|
||||
demander que l'ensemble des données associées aux formulaires soient
|
||||
retourné, plutôt qu'un jeu réduit adapté aux systèmes de synchronisation.
|
||||
Pour ce faire, il suffit de passer un paramètre <code>full=on</code> dans
|
||||
l’adresse.
|
||||
l'adresse.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/forms/inscriptions/list?full=on</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/forms/inscriptions/list?full=on<var>&signature…</var></input>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
À noter que pour ne pas alourdir l’export en mode <code>full=on</code>, le
|
||||
contenu des champs de type « Fichier » n’est pas exporté.
|
||||
À noter que pour ne pas alourdir l'export en mode <code>full=on</code>, les
|
||||
champs de type « Fichier » ne sont pas exportés.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Un paramètre <code>include-actions</code> permet d’inclure (<code>on</code>) ou
|
||||
non (<code>off</code>) la liste des actions globales et des déclencheurs de
|
||||
sauts automatiques actuellement accessible via l'API à l'utilisateur qui
|
||||
effectue la requête.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/forms/inscriptions/list?include-actions=on</input>
|
||||
</screen>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<title>Données anonymisées</title>
|
||||
|
||||
<p>
|
||||
Les API « Liste de formulaires » et le mode Pull de récupération d’un formulaire acceptent un
|
||||
Les API « Liste de formulaires » et le mode Pull de récupération d'un formulaire acceptent un
|
||||
paramètre supplémentaire <code>anonymise</code>. Quand celui-ci est présent des données anonymisées
|
||||
des formulaires sont renvoyées et les contrôles d’accès sont simplifiés à une signature simple, il
|
||||
n’est pas nécessaire de préciser l’identifiant d’un utilisateur.
|
||||
des formulaires sont renvoyées et les contrôles d'accès sont simplifiés à une signature simple, il
|
||||
n'est pas nécessaire de préciser l'identifiant d'un utilisateur.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/forms/inscriptions/list?full=on&anonymise</input>
|
||||
<input>GET https://www.example.net/api/forms/inscriptions/10/?anonymise</input>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
Par ailleurs, l’API « Liste de formulaires » accepte un paramètre
|
||||
<code>include-anonymised</code> permettant d’inclure (<code>on</code>) ou non
|
||||
(<code>off</code>) les demandes anonymisées dans la liste :
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/forms/inscriptions/list?include-anonymised=on</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/forms/inscriptions/list?full=on&anonymise<var>&signature…</var></input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/forms/inscriptions/10/?anonymise<var>&signature…</var></input>
|
||||
</screen>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="global-data">
|
||||
<title>Données de l’ensemble des formulaires</title>
|
||||
<title>Données de l'ensemble des formulaires</title>
|
||||
|
||||
<p>
|
||||
De manière similaire à l’API de récupération de la liste des demandes d’un
|
||||
formulaire, il est possible de récupérer l’ensemble des demandes de la
|
||||
De manière similaire à l'API de récupération de la liste des demandes d'un
|
||||
formulaire, il est possible de récupérer l'ensemble des demandes de la
|
||||
plateforme, peu importe leurs types.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/forms/</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/forms/<var>?signature…</var></input>
|
||||
</screen>
|
||||
|
||||
<code mime="application/json">
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"url": "https://www.example.net/inscriptions/1/",
|
||||
"last_update_time": "2015-03-26T23:08:45",
|
||||
"receipt_time": "2015-03-26T23:08:44",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"url": "https://www.example.net/inscriptions/3/",
|
||||
"last_update_time": "2015-03-27T12:11:21",
|
||||
"receipt_time": "2015-03-27T12:45:19",
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"url": "https://www.example.net/signalement/1/",
|
||||
"last_update_time": "2015-03-25T14:14:21",
|
||||
"receipt_time": "2015-03-25T14:48:20",
|
||||
"id": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
[
|
||||
{
|
||||
url: "https://www.example.net/inscriptions/1/",
|
||||
last_update_time: "2015-03-26T23:08:45",
|
||||
receipt_time: "2015-03-26T23:08:44",
|
||||
id: 1
|
||||
},
|
||||
{
|
||||
url: "https://www.example.net/inscriptions/3/",
|
||||
last_update_time: "2015-03-27T12:11:21",
|
||||
receipt_time: "2015-03-27T12:45:19",
|
||||
id: 3
|
||||
},
|
||||
{
|
||||
url: "https://www.example.net/signalement/1/",
|
||||
last_update_time: "2015-03-25T14:14:21",
|
||||
receipt_time: "2015-03-25T14:48:20",
|
||||
id: 1
|
||||
}
|
||||
]
|
||||
</code>
|
||||
|
||||
<p>
|
||||
Des paramètres peuvent être envoyés dans la requête pour filtrer les résultats.
|
||||
Il s’agit des mêmes paramètres que ceux du tableau global en backoffice.
|
||||
Il s'agit des mêmes paramètres que ceux du tableau global en backoffice.
|
||||
Par exemple, pour avoir une liste limitée aux demandes terminées :
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/forms/?status=done</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/forms/?status=done<var>&signature…</var></input>
|
||||
</screen>
|
||||
|
||||
<note><p>
|
||||
Le paramètre <code>full</code> n’est pas pris en charge dans cette API; le
|
||||
paramètre <code>anonymise</code> non plus, les données l’étant déjà.
|
||||
Le paramètre <code>full</code> n'est pas pris en charge dans cette API; le
|
||||
paramètre <code>anonymise</code> non plus, les données l'étant déjà.
|
||||
</p></note>
|
||||
|
||||
</section>
|
||||
|
@ -449,7 +423,8 @@ webservice <code>/geojson</code>.
|
|||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/forms/inscriptions/geojson</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/forms/inscriptions/geojson<var>?signature…</var></input>
|
||||
<output>{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
|
@ -472,20 +447,21 @@ webservice <code>/geojson</code>.
|
|||
</screen>
|
||||
|
||||
<p>
|
||||
De manière identique aux appels précédents, des filtres peuvent être passés dans l’URL.
|
||||
De manière identique aux appels précédents, des filtres peuvent être passés dans l'URL.
|
||||
</p>
|
||||
|
||||
<note><p>
|
||||
Les URL retournées pour les demandes pointent vers l’interface de gestion de celles-ci.
|
||||
Les URL retournées pour les demandes pointent vers l'interface de gestion de celles-ci.
|
||||
</p></note>
|
||||
|
||||
<p>
|
||||
Il est également possible d’obtenir les informations géographiques de
|
||||
l’ensemble des demandes :
|
||||
Il est également possible d'obtenir les informations géographiques de
|
||||
l'ensemble des demandes :
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/forms/geojson</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/forms/geojson<var>?signature…</var></input>
|
||||
</screen>
|
||||
|
||||
</section>
|
||||
|
@ -494,28 +470,20 @@ l’ensemble des demandes :
|
|||
<title>Code de suivi</title>
|
||||
|
||||
<p>
|
||||
Une API existe pour déterminer l’existence d’un code de suivi et, le cas
|
||||
Une API existe pour déterminer l'existence d'un code de suivi et, le cas
|
||||
échéant, découvrir la demande associée.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/code/QRFPTSLR</input>
|
||||
<output>{"url": "...",
|
||||
"load_url": "...",
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/code/QRFPTSLR<var>?signature…</var></input>
|
||||
<output>{"url": "http://www.example.net/demarche/23",
|
||||
"load_url": "http://www.example.net/code/QRFPTSLR/load",
|
||||
"err": 0}</output>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
Dans l’attribut <code>url</code> se trouvera l’adresse native de la demande,
|
||||
qui demandera authentification de l’utilisateur, et dans l’attribut
|
||||
<code>load_url</code> une adresse permettant de charger la demande sur la seule
|
||||
foi de l’accès. Il est important d’utiliser cette adresse et de ne pas essayer
|
||||
de la construire manuellement avec le code de suivi car elle peut évoluer. Pour
|
||||
cette même raison elle devrait être utilisée immédiatement, sans être stockée.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
En cas d’inexistence du code de suivi donné, une réponse avec un code de retour
|
||||
En cas d'inexistence du code de suivi donné, une réponse avec un code de retour
|
||||
404 est retourné.
|
||||
</p>
|
||||
|
||||
|
|
|
@ -15,27 +15,35 @@
|
|||
<title>Introduction aux API</title>
|
||||
|
||||
<p>
|
||||
Cette section de la documentation s’adresse aux développeurs
|
||||
d’applications tierces désirant interfacer celles-ci avec w.c.s.
|
||||
Cette section de la documentation s'adresse aux développeurs
|
||||
d'applications tierces désirant interfacer celles-ci avec w.c.s.
|
||||
</p>
|
||||
|
||||
<section id="tech">
|
||||
<title>Aspects techniques</title>
|
||||
|
||||
<p>
|
||||
L’API Web Services est constituée d’appels REST, qui sont idéalement effectués
|
||||
L'API Web Services est constituée d'appels REST, qui sont idéalement effectués
|
||||
en HTTPS, pour assurer la sécurité et la confidentialité des échanges. Le
|
||||
format d’échange des données est JSON. Ces deux propriétés la rendent
|
||||
format d'échange des données est JSON. Ces deux propriétés la rendent
|
||||
accessible facilement à tous les langages et environnements de programmation
|
||||
modernes.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Cette documentation se veut facile à lire, avec beaucoup de notes et
|
||||
d’exemples. Les différentes pages détaillent les points d’accès à
|
||||
d'exemples. Les différentes pages détaillent les points d'accès à
|
||||
utiliser pour réaliser les différentes opérations.
|
||||
</p>
|
||||
|
||||
<note>
|
||||
<p>
|
||||
Les exemples donnés dans ce document utilisent pour la plupart l'outil en
|
||||
ligne de commande <app>curl</app> qui permet de manière simple l'envoi de
|
||||
requêtes HTTP à un serveur.
|
||||
</p>
|
||||
</note>
|
||||
|
||||
</section>
|
||||
|
||||
</page>
|
||||
|
|
|
@ -19,20 +19,28 @@ w.c.s expose une API permettant aux logiciels tiers de connaître les différent
|
|||
formulaires et leurs schémas de données.
|
||||
</p>
|
||||
|
||||
<note><p>Toutes ces URL sont conformes à la spécification de remontée d'information du
|
||||
<em>Portail citoyen</em>, acceptent ainsi un paramètre <code>email</code> ou
|
||||
<code>NameID</code>, et nécessitent alors un paramètre <code>orig</code>.
|
||||
</p></note>
|
||||
|
||||
|
||||
<section id="forms">
|
||||
<title>Formulaires</title>
|
||||
|
||||
<p>
|
||||
La liste des formulaires accessibles à un utilisateur est disponible à
|
||||
l’URL <code>/api/formdefs/</code>.
|
||||
l'URL <code>/api/formdefs/</code>.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/formdefs/</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/formdefs/<var>?signature…</var></input>
|
||||
<output>
|
||||
[{"url": "https://www.example.net/inscriptions/newsletter",
|
||||
"title": "Newsletter",
|
||||
"slug": "newsletter",
|
||||
"count": 17,
|
||||
"authentication_required": false,
|
||||
"redirection": false,
|
||||
"description": "",
|
||||
|
@ -42,6 +50,7 @@ l’URL <code>/api/formdefs/</code>.
|
|||
{"url": "https://www.example.net/inscriptions/piscine",
|
||||
"title": "Piscine",
|
||||
"slug": "piscine",
|
||||
"count": 6,
|
||||
"authentication_required": true,
|
||||
"redirection": false,
|
||||
"description": "La piscine est ouverte du lundi au samedi.",
|
||||
|
@ -62,25 +71,14 @@ URL <code>/json</code> autrement.
|
|||
|
||||
<p>
|
||||
La liste des formulaires accessibles à un utilisateur dans le but de faire une
|
||||
saisie backoffice est disponible, sous le même format, via l’URL
|
||||
saisie backoffice est disponible, sous le même format, via l'URL
|
||||
<code>/api/formdefs/?backoffice-submission=on</code>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Il est également possible d’obtenir un nombre permettant de trier les résultats
|
||||
Il est également possible d'obtenir un nombre permettant de trier les résultats
|
||||
par « popularité » en ajoutant un paramètre <code>include-count=on</code>. Les
|
||||
différentes entrées disposeront alors d’une clé <code>count</code>.
|
||||
</p>
|
||||
|
||||
<note style="important">
|
||||
<p>L’information <code>count</code> n’est <em>pas</em> le décompte intégral des
|
||||
demandes, c’est un indice composite donnant davantage de poids aux demandes
|
||||
récentes.</p>
|
||||
</note>
|
||||
|
||||
<p>
|
||||
La liste retournée inclura les formulaires désactivés en ajoutant le paramètre
|
||||
<code>include-disabled=on</code>.
|
||||
différentes entrées disposeront alors d'une clé <code>count</code>.
|
||||
</p>
|
||||
|
||||
</section>
|
||||
|
@ -90,11 +88,12 @@ La liste retournée inclura les formulaires désactivés en ajoutant le paramèt
|
|||
<title>Catégories</title>
|
||||
|
||||
<p>
|
||||
La liste des catégories est disponible à l’URL <code>/api/categories/</code>.
|
||||
La liste des catégories est disponible à l'URL <code>/api/categories/</code>.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/categories/</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/categories/<var>?signature…</var></input>
|
||||
<output>
|
||||
{"data":
|
||||
[
|
||||
|
@ -111,23 +110,24 @@ La liste des catégories est disponible à l’URL <code>/api/categories/</code>
|
|||
</screen>
|
||||
|
||||
<p>
|
||||
Il est possible de passer un paramètre <code>full=on</code> dans l’adresse pour
|
||||
obtenir pour chacune des catégories la liste des formulaires qu’elle contient,
|
||||
Il est possible de passer un paramètre <code>full=on</code> dans l'adresse pour
|
||||
obtenir pour chacune des catégories la liste des formulaires qu'elle contient,
|
||||
dans une clé supplémentaire, <code>forms</code>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Les formulaires d’une catégorie précise sont disponibles à l’URL
|
||||
Les formulaires d'une catégorie précise sont disponibles à l'URL
|
||||
<code>/api/categories/<var>slug</var>/formdefs/</code>.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/categories/inscriptions/formdefs/</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/categories/inscriptions/formdefs/<var>?signature…</var></input>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
Comme pour la liste des formulaires en général, on peut ajouter l’argument
|
||||
<code>?backoffice-submission=on</code> à cette URL, pour n’obtenir que les
|
||||
Comme pour la liste des formulaires en général, on peut ajouter l'argument
|
||||
<code>?backoffice-submission=on</code> à cette URL, pour n'obtenir que les
|
||||
formulaires de la catégorie accessibles en saisie backoffice.
|
||||
</p>
|
||||
|
||||
|
@ -139,11 +139,12 @@ formulaires de la catégorie accessibles en saisie backoffice.
|
|||
<title>Rôles</title>
|
||||
|
||||
<p>
|
||||
La liste des rôles est disponible à l’URL <code>/api/roles</code>.
|
||||
La liste des rôles est disponible à l'URL <code>/api/roles</code>.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/roles</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/roles<var>?signature…</var></input>
|
||||
<output>
|
||||
{"data":
|
||||
[
|
||||
|
@ -162,23 +163,21 @@ La liste des rôles est disponible à l’URL <code>/api/roles</code>.
|
|||
|
||||
|
||||
<section id="data-schema">
|
||||
<title>Schéma de données d’un formulaire</title>
|
||||
<title>Schéma de données d'un formulaire</title>
|
||||
|
||||
<p>
|
||||
Le schéma de données d’un formulaire est accessible à l’adresse
|
||||
<code>/api/formdefs/<em>slug</em>/schema</code>; l’appel doit obligatoirement
|
||||
être signé ou réalisé avec un accès disposant des rôles de gestion sur le
|
||||
formulaire.
|
||||
Le schéma de données d'un formulaire est accessible à l'adresse
|
||||
<code>/api/formdefs/<em>slug</em>/schema</code>.
|
||||
|
||||
</p>
|
||||
|
||||
<code mime="application/json">
|
||||
{
|
||||
"name": "Newsletter",
|
||||
"only_allow_one": false,
|
||||
"enable_tracking_codes": true,
|
||||
"tracking_code_verify_fields": ["1"],
|
||||
"confirmation": true,
|
||||
"discussion": false,
|
||||
"only_allow_one": "false",
|
||||
"enable_tracking_codes": "true",
|
||||
"confirmation": "true",
|
||||
"discussion": "false",
|
||||
"fields": [
|
||||
{
|
||||
"label": "Nom",
|
||||
|
@ -228,7 +227,7 @@ formulaire.
|
|||
<note>
|
||||
<p>
|
||||
Note de compatibilité : la même information est disponible en ajoutant
|
||||
<code>/schema</code> à l’adresse publique du formulaire, par exemple
|
||||
<code>/schema</code> à l'adresse publique du formulaire, par exemple
|
||||
<code>http://www.example.net/inscriptions/newsletter<em>/schema</em></code>.
|
||||
</p>
|
||||
</note>
|
||||
|
|
|
@ -8,28 +8,61 @@
|
|||
<name>Frédéric Péters</name>
|
||||
<email>fpeters@entrouvert.com</email>
|
||||
</credit>
|
||||
<desc>Demandes et brouillons d’un usager</desc>
|
||||
<desc>Profil utilisateur, formulaires associés, etc.</desc>
|
||||
|
||||
</info>
|
||||
|
||||
<title>Récupération des données d’un usager</title>
|
||||
<title>Récupération des données d'un utilisateur</title>
|
||||
|
||||
<p>
|
||||
Il s’agit ici des API permettant à un logiciel tiers de récupérer les données
|
||||
associées aux usagers enregistrés.
|
||||
Il s'agit ici d'API permettant à un logiciel tiers de récupérer les données
|
||||
associées aux utilisateurs enregistrés.
|
||||
</p>
|
||||
|
||||
<section id="forms">
|
||||
<title>Demandes</title>
|
||||
<section>
|
||||
<title>Profil</title>
|
||||
|
||||
<p>
|
||||
La liste des demandes transmises par un usager est accessible à l’URL
|
||||
<code>/api/users/<var>uuid</var>/forms</code>, elle reprend un ensemble
|
||||
minimal d’informations concernant chacune de celles-ci.
|
||||
</p>
|
||||
<p>
|
||||
Ces accès doivent se faire en passant les informations d'identification
|
||||
appropriées dans la <em>query string</em>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Les informations associées à un utilisateur sont accessibles à l'URL
|
||||
<code>/api/user/</code>, elles reprennent son nom (<code>user_display_name</code>),
|
||||
son adresse électronique (<code>user_email</code>) ainsi que ses éventuelles
|
||||
autorisations d'accès au backoffice (<code>user_backoffice_access</code>) ou
|
||||
à l'interface d'administration (<code>user_admin_access</code>).
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/users/<var>uuid</var>/forms</input>
|
||||
<output style="prompt">$ </output><input>curl https://www.example.net/api/user/<var>?signature…</var></input>
|
||||
<output>{
|
||||
"user_display_name": "Fred Cuadrado",
|
||||
"user_email": "fred@example.net",
|
||||
"user_backoffice_access": true,
|
||||
"user_admin_access": false
|
||||
}
|
||||
</output></screen>
|
||||
|
||||
<note>
|
||||
<p>Note de compatibilité : cette information est également disponible à
|
||||
l'adresse <code>/user</code>.
|
||||
</p>
|
||||
</note>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="forms">
|
||||
<title>Formulaires</title>
|
||||
|
||||
<p>
|
||||
La liste des formulaires transmis par un utilisateur est accessible à l'URL
|
||||
<code>/api/user/forms</code>, elle reprend un ensemble minimal
|
||||
d'informations concernant chacun de ceux-ci.
|
||||
</p>
|
||||
<screen>
|
||||
<output style="prompt">$ </output><input>curl https://www.example.net/api/user/forms<var>?signature…</var></input>
|
||||
<output>{
|
||||
"err": 0,
|
||||
"data": [
|
||||
|
@ -37,7 +70,7 @@ associées aux usagers enregistrés.
|
|||
"category_id": "1",
|
||||
"category_name": "Divers",
|
||||
"datetime": "2014-03-28 15:36:52",
|
||||
"form_name": "Demande d’inscription",
|
||||
"form_name": "Demande d'inscription",
|
||||
"form_slug": "demande-d-inscription",
|
||||
"form_number": "123",
|
||||
"form_number_raw": "123",
|
||||
|
@ -48,9 +81,9 @@ associées aux usagers enregistrés.
|
|||
"form_uri": "demande-d-inscription/123/",
|
||||
"form_url": "http://www.example.net/demande-d-inscription/123/",
|
||||
"form_url_backoffice": "http://www.example.net/backoffice/demande-d-inscription/123/",
|
||||
"name": "Demande d’inscription",
|
||||
"name": "Demande d'inscription",
|
||||
"status": "Nouveau",
|
||||
"title": "Demande d’inscription #123 (Nouveau)",
|
||||
"title": "Demande d'inscription #123 (Nouveau)",
|
||||
"url": "http://www.example.net/demande-d-inscription/123/",
|
||||
},
|
||||
{
|
||||
|
@ -77,7 +110,7 @@ associées aux usagers enregistrés.
|
|||
"category_id": "3",
|
||||
"category_name": "Modification de vos coordonn\u00e9es",
|
||||
"datetime": "2014-03-17 10:42:17",
|
||||
"form_name": "Changement d’adresse",
|
||||
"form_name": "Changement d'adresse",
|
||||
"form_slug": "changement-d-adresse",
|
||||
"form_number": "424",
|
||||
"form_number_raw": "424",
|
||||
|
@ -88,30 +121,18 @@ associées aux usagers enregistrés.
|
|||
"form_uri": "changement-d-adresse/424/",
|
||||
"form_url": "http://www.example.net/changement-d-adresse/424/",
|
||||
"form_url_backoffice": "http://www.example.net/backoffice/changement-d-adresse/424/",
|
||||
"name": "Changement d’adresse",
|
||||
"name": "Changement d'adresse",
|
||||
"status": "Traitement de la demande termin\u00e9",
|
||||
"title": "Changement d’adresse #424 (Traitement de la demande termin\u00e9)",
|
||||
"title": "Changement d'adresse #424 (Traitement de la demande termin\u00e9)",
|
||||
"url": "http://www.example.net/changement-d-adresse/424/",
|
||||
}
|
||||
]
|
||||
}</output></screen>
|
||||
|
||||
<note><p>
|
||||
Le même résultat peut être obtenu en utilisant <code>/api/user/forms</code>
|
||||
mais cet endpoint ne fonctionne pas avec l’authentification HTTP Basique;
|
||||
elle demande la mise en place de l’algorithme de signature.
|
||||
</p></note>
|
||||
|
||||
<p>
|
||||
Il est possible de recevoir un ensemble plus complet de données en passant un
|
||||
paramètre <code>full=on</code> à l’adresse. Pour inclure également les
|
||||
paramètre <code>full=on</code> à l'adresse. Pour inclure également les
|
||||
brouillons, un paramètre <code>include-drafts=true</code> peut être passé.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Par ailleurs le filtre <code>?filter-user-uuid=</code> peut s’appliquer sur
|
||||
les API de récupérations de demandes et de fiches pour filtrer sur un usager
|
||||
particulier.
|
||||
</p>
|
||||
|
||||
</section>
|
||||
|
@ -119,12 +140,12 @@ particulier.
|
|||
<title>Brouillons</title>
|
||||
|
||||
<p>
|
||||
La liste des brouillons de l’usager est accessible à l’adresse
|
||||
<code>/api/users/<var>uuid</var>/drafts</code>.
|
||||
La liste des brouillons de l'utilisateur est accessible à l'adresse
|
||||
<code>/api/user/drafts</code>.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>GET https://www.example.net/api/user/<var>uuid</var>/drafts</input>
|
||||
<output style="prompt">$ </output><input>curl https://www.example.net/api/user/drafts<var>?signature…</var></input>
|
||||
<output>{
|
||||
"err": 0,
|
||||
"data": [
|
||||
|
@ -137,6 +158,43 @@ particulier.
|
|||
]
|
||||
}</output></screen>
|
||||
|
||||
<note>
|
||||
<p>Note de compatibilité : cette information est également disponible à
|
||||
l'adresse <code>/myspace/drafts</code>.
|
||||
</p>
|
||||
</note>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<section>
|
||||
<title>Liste des utilisateurs</title>
|
||||
|
||||
<p>
|
||||
La liste des utilisateurs est disponible à l'URL <code>/api/users/</code>,
|
||||
il est possible de la filtrer, sur le nom ou l'adresse électronique, en
|
||||
spécifiant un paramètre <code>q</code> et de limiter le nombre de
|
||||
résultats obtenus avec le paramètre <code>limit</code>.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
|
||||
https://www.example.net/api/users/?q=fred<var>&signature…</var></input>
|
||||
<output>
|
||||
{
|
||||
"err": 0,
|
||||
"data": [
|
||||
{
|
||||
"user_display_name": "Fred",
|
||||
"user_email": "fred@example.net",
|
||||
"user_backoffice_access": true,
|
||||
"user_admin_access": false
|
||||
}
|
||||
}
|
||||
}
|
||||
</output>
|
||||
</screen>
|
||||
|
||||
</section>
|
||||
|
||||
</page>
|
||||
|
|
|
@ -12,16 +12,16 @@
|
|||
|
||||
</info>
|
||||
|
||||
<title>Traitement d’un formulaire</title>
|
||||
<title>Traitement d'un formulaire</title>
|
||||
|
||||
<section>
|
||||
<title>Synchrone</title>
|
||||
|
||||
<p>
|
||||
Pour faire évoluer un formulaire en fonction de données extérieures, le
|
||||
workflow peut contenir une action d’<link xref="wf-wscall">appel à un
|
||||
workflow peut contenir une action d'<link xref="wf-wscall">appel à un
|
||||
webservice</link> et enchaîner sur une série de sauts automatiques,
|
||||
conditionnés par le résultat de l’appel.
|
||||
conditionnés par le résultat de l'appel.
|
||||
</p>
|
||||
|
||||
</section>
|
||||
|
@ -33,34 +33,37 @@ conditionnés par le résultat de l’appel.
|
|||
<p>
|
||||
Pour des traitements asynchrones, w.c.s expose également une API autorisant
|
||||
les logiciels tiers à faire progresser le
|
||||
traitement d’un formulaire; cela passe par la définition dans le statut du
|
||||
workflow d’un élément de type « Changement de statut automatique », dans
|
||||
traitement d'un formulaire; cela passe par la définition dans le statut du
|
||||
workflow d'un élément de type « Changement de statut automatique », dans
|
||||
lequel un identifiant de déclencheur est défini.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
La demande d’un changement d’état se fait par une requête <code>POST</code> à
|
||||
l’adresse du formulaire en question, suivi de <code>jump/trigger/</code> et de
|
||||
la référence à l’identifiant de déclencheur (<code>validate</code> dans
|
||||
l’exemple qui suit).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Lors de cette requête, il est nécessaire d’inclure l’entête
|
||||
<code>Accept: application/json</code>.
|
||||
La demande d'un changement d'état se fait par une requête <code>POST</code> à
|
||||
l'adresse du formulaire en question, suivi de <code>jump/trigger/</code> et de
|
||||
la référence à l'identifiant de déclencheur (<code>validate</code> dans
|
||||
l'exemple qui suit).
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<input>POST https://www.example.net/inscriptions/newsletter/14/jump/trigger/validate/</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" -X POST \
|
||||
https://www.example.net/inscriptions/newsletter/14/jump/trigger/validate<var>?signature…</var></input>
|
||||
<output>{"url": null, "err": 0}</output>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
Il est également possible d’accompagner le déclenchement d’un changement
|
||||
de statut d’une série de données, qui seront enregistrées dans les données de
|
||||
Il est également possible d'accompagner le déclenchement d'un changement
|
||||
de statut d'une série de données, qui seront enregistrées dans les données de
|
||||
workflow du formulaire.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<output style="prompt">$ </output><input>curl -H "Content-type: application/json" -H "Accept: application/json" \
|
||||
-X POST -d@donnes.json \
|
||||
https://www.example.net/inscriptions/newsletter/14/jump/trigger/validate<var>?signature…</var></input>
|
||||
<output>{"url": null, "err": 0}</output>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
Il est également possible de définir des déclencheurs au niveau des actions
|
||||
globales du workflow, ils pourront alors être appelés quel que soit le statut
|
||||
|
@ -73,7 +76,8 @@ ferait ainsi :
|
|||
</p>
|
||||
|
||||
<screen>
|
||||
<input>POST https://www.example.net/api/forms/newsletter/14/hooks/urgent/</input>
|
||||
<output style="prompt">$ </output><input>curl -H "Accept: application/json" -X POST \
|
||||
https://www.example.net/api/forms/newsletter/14/hooks/urgent/<var>?signature…</var></input>
|
||||
<output>{"err": 0}</output>
|
||||
</screen>
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<title>Scripts externes</title>
|
||||
|
||||
<p>
|
||||
Il est possible d’étendre les capacités des champs calculés et des expressions
|
||||
Il est possible d'étendre les capacités des champs calculés et des expressions
|
||||
utilisées dans les gabarits au moyen de scripts externes. Pour cela il suffit
|
||||
de déposer dans le répertoire système du site, dans un sous-répertoire
|
||||
<code>scripts</code>, un fichier Python, dont le résultat doit être posé dans
|
||||
|
@ -29,7 +29,7 @@ Par exemple <file>/var/lib/wcs/www.example.net/scripts/hello.py</file> pourrait
|
|||
|
||||
<code mime="text/python">
|
||||
"""
|
||||
Salue l’usager (quand un nom est passé en argument), ou le monde.
|
||||
Salue l'usager (quand un nom est passé en argument), ou le monde.
|
||||
"""
|
||||
if args:
|
||||
result = "Hello %s" % args[0]
|
||||
|
@ -39,7 +39,7 @@ else:
|
|||
|
||||
<p>
|
||||
Dans un champ calculé, cela serait appelé comme <code>script.hello()</code> ou
|
||||
<code>script.hello('earth')</code>; dans un gabarit, il n’y a pas de prise en
|
||||
<code>script.hello('earth')</code>; dans un gabarit, il n'y a pas de prise en
|
||||
charge des arguments, la seule utilisation possible est
|
||||
<code>{{script.hello}}</code>.
|
||||
</p>
|
||||
|
@ -48,7 +48,7 @@ charge des arguments, la seule utilisation possible est
|
|||
<p>
|
||||
Il est également possible de placer ces scripts dans un sous-répertoire
|
||||
<code>scripts</code> du répertoire général des instances, pour rendre ceux-ci
|
||||
disponibles depuis l’ensemble des instances.
|
||||
disponibles depuis l'ensemble des instances.
|
||||
</p>
|
||||
</note>
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<p>
|
||||
Configuré en mode PostgreSQL, <app>w.c.s.</app> crée une série de <em>vues</em>
|
||||
permettant un accès aux données des différents formulaires.
|
||||
L’utilisation de ces vues est recommandée, l’accès direct aux tables étant
|
||||
L'utilisation de ces vues est recommandée, l'accès direct aux tables étant
|
||||
réservé aux usages internes.
|
||||
</p>
|
||||
|
||||
|
@ -26,8 +26,8 @@ réservé aux usages internes.
|
|||
|
||||
<p>
|
||||
Une vue nommée <code>wcs_view_<var>xx</var>_<var>libellé</var></code> (avec
|
||||
<var>xx</var> étant l’identifiant du formulaire et <var>libellé</var>
|
||||
étant son nom tel qu’il appararait dans les URL) est créée par type de
|
||||
<var>xx</var> étant l'identifiant du formulaire et <var>libellé</var>
|
||||
étant son nom tel qu'il appararait dans les URL) est créée par type de
|
||||
formulaire pour donner accès aux données de ceux-ci.
|
||||
</p>
|
||||
|
||||
|
@ -38,7 +38,7 @@ réservé aux usages internes.
|
|||
<list>
|
||||
<item><p><var>id</var> : identifiant interne</p></item>
|
||||
<item><p><var>id_display</var> : identifiant externe, le cas échéant</p></item>
|
||||
<item><p><var>user_id</var> : identifiant de l’utilisateur</p></item>
|
||||
<item><p><var>user_id</var> : identifiant de l'utilisateur</p></item>
|
||||
<item><p><var>receipt_time</var> : date et heure de réception</p></item>
|
||||
<item><p><var>status</var> : statut courant</p></item>
|
||||
<item><p><var>is_at_endpoint</var> : indicateur de fin de traitement</p></item>
|
||||
|
@ -46,14 +46,14 @@ réservé aux usages internes.
|
|||
<item><p><var>formdef_id</var> : identifiant du type de formulaire</p></item>
|
||||
<item><p><var>fts</var> : indexation texte intégral</p></item>
|
||||
<item><p><var>backoffice_submission</var> : indicateur de saisie backoffice</p></item>
|
||||
<item><p><var>submission_channel</var> : canal d’entrée</p></item>
|
||||
<item><p><var>submission_channel</var> : canal d'entrée</p></item>
|
||||
</list>
|
||||
|
||||
<p>
|
||||
Les différents champs du formulaire sont ensuite présents en autant de
|
||||
colonnes, elles sont nommées selon le format
|
||||
<code>f_<var>identifiant</var></code> où identifiant est le nom de variable
|
||||
utilisé dans la définition du champ. Quand celui-ci n’est pas défini, la
|
||||
utilisé dans la définition du champ. Quand celui-ci n'est pas défini, la
|
||||
colonne est nommée
|
||||
<code>f_<var>identifiant-numérique-interne</var>_<var>libellé</var></code>.
|
||||
Pour un certain nombre de champs, différenciant la valeur présentée de la
|
||||
|
@ -63,7 +63,7 @@ réservé aux usages internes.
|
|||
|
||||
<p>
|
||||
Un dernier champ, <var>status_history</var> reprend un tableau avec
|
||||
l’historique des statuts par lesquels le formulaire est passé.
|
||||
l'historique des statuts par lesquels le formulaire est passé.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
@ -71,21 +71,21 @@ réservé aux usages internes.
|
|||
<title>Agrégation de formulaires</title>
|
||||
|
||||
<p>
|
||||
Les champs communs à l’ensemble des formulaires, c’est-à-dire ceux repris
|
||||
Les champs communs à l'ensemble des formulaires, c'est-à-dire ceux repris
|
||||
dans la première liste donnée ci-dessus (<em>id</em>,
|
||||
<em>id_display</em>…), sont également agrégés dans une vue unique,
|
||||
<code>wcs_all_forms</code>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
De la même manière, les formulaires tirés d’une même catégorie sont agrégés
|
||||
De la même manière, les formulaires tirés d'une même catégorie sont agrégés
|
||||
dans une vue nommée <code>wcs_category_<var>libellé</var></code> (ou
|
||||
<var>libellé</var> correspond au titre de la catégorie).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Ces agrégations sont utiles pour permettre la réalisation d’opérations sur
|
||||
l’ensemble des formulaires.
|
||||
Ces agrégations sont utiles pour permettre la réalisation d'opérations sur
|
||||
l'ensemble des formulaires.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
|
|
|
@ -14,25 +14,25 @@
|
|||
<title>Géolocalisation</title>
|
||||
|
||||
<p>
|
||||
Un champ de type « Carte » permet d’afficher à l’usager une carte dans
|
||||
Un champ de type « Carte » permet d'afficher à l'usager une carte dans
|
||||
laquelle il pourra pointer une adresse.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Le paramétrage du champ permet de préciser la zone sur laquelle la carte sera
|
||||
centrée à l’ouverture du formulaire, ainsi que le niveau de zoom initial,
|
||||
parce qu’il est inutile de présenter tout un pays dans un formulaire permettant
|
||||
centrée à l'ouverture du formulaire, ainsi que le niveau de zoom initial,
|
||||
parce qu'il est inutile de présenter tout un pays dans un formulaire permettant
|
||||
de signaler un trou dans une route de la ville, et des limites aux zooms,
|
||||
parce qu’à nouveau, inutile de perdre l’usager dans un zoom affichant la terre
|
||||
entière, ou le moindre brin d’herbe.
|
||||
parce qu'à nouveau, inutile de perdre l'usager dans un zoom affichant la terre
|
||||
entière, ou le moindre brin d'herbe.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
En alternative à une zone initiale fixe, il est également possible de demander
|
||||
à l’appareil mobile de l’usager sa position (qui sera alors déterminée par
|
||||
à l'appareil mobile de l'usager sa position (qui sera alors déterminée par
|
||||
GPS, Wifi ou autre), et de centrer la carte sur celle-ci. Via les options de
|
||||
préremplissage, il peut aussi être demandé d’automatiquement sélectionner
|
||||
comme point la position courante de l’usager.
|
||||
préremplissage, il peut aussi être demandé d'automatiquement sélectionner
|
||||
comme point la position courante de l'usager.
|
||||
</p>
|
||||
|
||||
<section>
|
||||
|
@ -40,9 +40,9 @@
|
|||
|
||||
<p>
|
||||
Les autres champs du formulaire peuvent être remplis selon le point qui sera
|
||||
sélectionné par l’usager sur la carte; pour ce faire dans leurs options de
|
||||
sélectionné par l'usager sur la carte; pour ce faire dans leurs options de
|
||||
préremplissage il suffit de sélectionner « Géolocalisation » et de choisir
|
||||
l’élément d’adresse souhaité comme contenu : la rue ou le numéro, ou les
|
||||
l'élément d'adresse souhaité comme contenu : la rue ou le numéro, ou les
|
||||
deux, le code postal, la ville, la région.
|
||||
</p>
|
||||
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
|
||||
<p>
|
||||
Dans bien des situations il est nécessaire de structurer un formulaire en
|
||||
plusieurs pages, pour ce faire un type de champ spécial existe, il s’agit de
|
||||
plusieurs pages, pour ce faire un type de champ spécial existe, il s'agit de
|
||||
« Page ». Il permet de définir un titre aux pages, qui sera affiché à
|
||||
l’usager dans l’indicateur de progression.
|
||||
l'usager dans l'indicateur de progression.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
@ -30,9 +30,9 @@ trouvent pas à <em>flotter</em> en-dehors de la structure de pages.
|
|||
|
||||
<note>
|
||||
<p>
|
||||
Afin de rappeler l’importance de la définition d’une page comme premier
|
||||
élément de formulaire, un message d’information est affiché en haut de la
|
||||
définition des champs quand ce n’est pas le cas.
|
||||
Afin de rappeler l'importance de la définition d'une page comme premier
|
||||
élément de formulaire, un message d'information est affiché en haut de la
|
||||
définition des champs quand ce n'est pas le cas.
|
||||
</p>
|
||||
</note>
|
||||
|
||||
|
@ -40,37 +40,37 @@ trouvent pas à <em>flotter</em> en-dehors de la structure de pages.
|
|||
<title>Pages conditionnelles</title>
|
||||
|
||||
<p>
|
||||
Dans certaines situations toutes les pages d’un formulaire n’ont pas à être
|
||||
Dans certaines situations toutes les pages d'un formulaire n'ont pas à être
|
||||
présentées dans toutes les situations, inutile par exemple de présenter une
|
||||
page précisant les modalités d’accès à un parking si l’usager a noté dans
|
||||
une page précédente qu’il viendrait en tant que piéton.
|
||||
page précisant les modalités d'accès à un parking si l'usager a noté dans
|
||||
une page précédente qu'il viendrait en tant que piéton.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Pour répondre à ce besoin, en plus du libellé de la page, les champs de type
|
||||
« Page » disposent d’une option permettant d’en conditionner
|
||||
l’affichage.
|
||||
« Page » disposent d'une option permettant d'en conditionner
|
||||
l'affichage.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Une condition s’exprime sous forme d’une <em>expression Python</em>, qui peut
|
||||
Une condition s'exprime sous forme d'une <em>expression Python</em>, qui peut
|
||||
faire référence à des informations concernant le formulaire en cours de
|
||||
remplissage mais aussi à l’usager occupé à le remplir.
|
||||
remplissage mais aussi à l'usager occupé à le remplir.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Pour partir sur la situation du premier paragraphe, le champ « mode de
|
||||
transport » pourrait avoir comme nom de variable associé
|
||||
<code>mode_de_transport</code>, la page sur l’accès au parking ne devrait
|
||||
pas être affichée pour les personnes ayant précisés qu’elles venaient à
|
||||
pieds, le champ serait complété avec l’expression suivante :
|
||||
<code>mode_de_transport</code>, la page sur l'accès au parking ne devrait
|
||||
pas être affichée pour les personnes ayant précisés qu'elles venaient à
|
||||
pieds, le champ serait complété avec l'expression suivante :
|
||||
<code>form_var_mode_de_transport != "Piéton"</code> (où le <code>!=</code>
|
||||
correspond à la syntaxe Python signifiant « différent de »).
|
||||
</p>
|
||||
|
||||
<note>
|
||||
<p>
|
||||
D’autres exemples de condition sont présentés dans la page <link
|
||||
D'autres exemples de condition sont présentés dans la page <link
|
||||
xref="misc-conditions"/>.
|
||||
</p>
|
||||
</note>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<info>
|
||||
<revision docversion="0.1" date="2013-01-04" status="draft"/>
|
||||
<license>
|
||||
<p>Ce travail est publié à la fois sous <link href="http://creativecommons.org/licenses/by-sa/3.0/fr/">licence Creatice Commons Paternité - Partage à l’Identique 3.0</link> et sous <link href="http://www.gnu.org/licenses/gpl.html">licence GPL v3.</link></p>
|
||||
<p>Ce travail est publié à la fois sous <link href="http://creativecommons.org/licenses/by-sa/3.0/fr/">licence Creatice Commons Paternité - Partage à l'Identique 3.0</link> et sous <link href="http://www.gnu.org/licenses/gpl.html">licence GPL v3.</link></p>
|
||||
</license>
|
||||
|
||||
<credit type="author">
|
||||
|
@ -19,7 +19,7 @@
|
|||
w.c.s est un logiciel permettant de générer des formulaires et des
|
||||
consultations en ligne et de les intégrer dans un workflow. Configuré par
|
||||
défaut pour des besoins basique, il offre une personnalisation poussée
|
||||
permettant de l’adapter à de nombreux usages différents.
|
||||
permettant de l'adapter à de nombreux usages différents.
|
||||
</p>
|
||||
|
||||
<section id="form" style="2column">
|
||||
|
@ -29,7 +29,7 @@ permettant de l’adapter à de nombreux usages différents.
|
|||
<title>Atelier de formulaires</title>
|
||||
<p>
|
||||
Le cœur de métier de <app>w.c.s.</app> est la définition de formulaires, des
|
||||
plus simples aux plus complexes; une variété de possibilités et d’options
|
||||
plus simples aux plus complexes; une variété de possibilités et d'options
|
||||
existent.
|
||||
</p>
|
||||
</section>
|
||||
|
@ -41,7 +41,7 @@ permettant de l’adapter à de nombreux usages différents.
|
|||
<title>Atelier de workflows</title>
|
||||
<p>
|
||||
Pour définir des logiques de traitement complexe, w.c.s. intègre un
|
||||
mécanisme de workflow, permettant de définir une série d’états et d’actions
|
||||
mécanisme de workflow, permettant de définir une série d'états et d'actions
|
||||
associées.
|
||||
</p>
|
||||
</section>
|
||||
|
@ -52,9 +52,9 @@ permettant de l’adapter à de nombreux usages différents.
|
|||
</info>
|
||||
<title>Paramétrage système</title>
|
||||
<p>
|
||||
Pour l’administration système, <app>w.c.s.</app> dispose d’une série de
|
||||
Pour l'administration système, <app>w.c.s.</app> dispose d'une série de
|
||||
paramètres concernant son fonctionnement et son intégration dans le système
|
||||
d’information.
|
||||
d'information.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
|
||||
<p>
|
||||
Dans la définition de pages conditionnelles et dans le paramétrage de sauts
|
||||
dans le workflow, il y a besoin d’exprimer une condition sous forme d’une
|
||||
dans le workflow, il y a besoin d'exprimer une condition sous forme d'une
|
||||
expression Python. Cette page ne se veut évidemment pas un guide complet,
|
||||
toutes les possibilités offertes par le langage Python étant possibles, mais
|
||||
une série d’exemples concrets, rencontrés dans le paramétrage.
|
||||
une série d'exemples concrets, rencontrés dans le paramétrage.
|
||||
</p>
|
||||
|
||||
<note><p>Une explication sur les variables accessibles se trouve dans la page
|
||||
|
@ -29,15 +29,15 @@ une série d’exemples concrets, rencontrés dans le paramétrage.
|
|||
<title>Expressions simples</title>
|
||||
|
||||
<p>
|
||||
Pour tester le code postal associé à l’utilisateur courant, par exemple
|
||||
pour proposer une page différente aux habitants d’une commune particulière,
|
||||
l’expression suivante pourrait être utilisée :
|
||||
Pour tester le code postal associé à l'utilisateur courant, par exemple
|
||||
pour proposer une page différente aux habitants d'une commune particulière,
|
||||
l'expression suivante pourrait être utilisée :
|
||||
</p>
|
||||
|
||||
<example><code>session_user_var_codepostal == '07530'</code></example>
|
||||
|
||||
<p>
|
||||
Dans le même ordre d’idée, pour les utilisateurs qui ne seraient <em>pas</em>
|
||||
Dans le même ordre d'idée, pour les utilisateurs qui ne seraient <em>pas</em>
|
||||
de cette commune :
|
||||
</p>
|
||||
|
||||
|
@ -45,7 +45,7 @@ une série d’exemples concrets, rencontrés dans le paramétrage.
|
|||
|
||||
<p>
|
||||
De manière générale, la négative peut également être obtenue en entourant
|
||||
l’expression d’un <code>not()</code> :
|
||||
l'expression d'un <code>not()</code> :
|
||||
</p>
|
||||
|
||||
<example><code>not(session_user_var_codepostal == '07530')</code></example>
|
||||
|
@ -74,8 +74,8 @@ une série d’exemples concrets, rencontrés dans le paramétrage.
|
|||
|
||||
<p>
|
||||
Tout autre chose, pour une réservation, on pourrait vouloir afficher une
|
||||
page supplémentaire demandant les noms des inscrits, quand l’inscription
|
||||
est faite pour plusieurs personnes. L’utilisation de <code>int()</code>
|
||||
page supplémentaire demandant les noms des inscrits, quand l'inscription
|
||||
est faite pour plusieurs personnes. L'utilisation de <code>int()</code>
|
||||
permet les comparaisons numériques :
|
||||
</p>
|
||||
|
||||
|
@ -103,7 +103,7 @@ une série d’exemples concrets, rencontrés dans le paramétrage.
|
|||
<example><code>session_user_var_codepostal in ('07510', '07520', '07530')</code></example>
|
||||
|
||||
<p>
|
||||
Sur ces exemples de codes postaux, si l’application visait plusieurs pays, on
|
||||
Sur ces exemples de codes postaux, si l'application visait plusieurs pays, on
|
||||
devrait combiner le test, pour par exemple avoir « 07530 » comme code postal
|
||||
<em>et</em> « France » comme pays. Cela se fait avec le mot-clé
|
||||
<code>and</code> :
|
||||
|
@ -117,7 +117,7 @@ une série d’exemples concrets, rencontrés dans le paramétrage.
|
|||
<title>Condition sur un âge ou un délai</title>
|
||||
|
||||
<p>
|
||||
Pour limiter une condition aux enfants dont l’âge est inférieur à 6 ans vous
|
||||
Pour limiter une condition aux enfants dont l'âge est inférieur à 6 ans vous
|
||||
pouvez utiliser la fonction <code>utils.age_in_years</code> sur une variable de
|
||||
type date.
|
||||
</p>
|
||||
|
@ -126,7 +126,7 @@ une série d’exemples concrets, rencontrés dans le paramétrage.
|
|||
|
||||
<p>
|
||||
Pour prendre en compte le nombre de mois vous disposez aussi de
|
||||
<code>utils.age_in_years_and_months</code>. Ici l’âge à ne pas dépasser sera 6 ans
|
||||
<code>utils.age_in_years_and_months</code>. Ici l'âge à ne pas dépasser sera 6 ans
|
||||
et 3 mois.
|
||||
</p>
|
||||
|
||||
|
@ -134,7 +134,7 @@ une série d’exemples concrets, rencontrés dans le paramétrage.
|
|||
|
||||
<p>
|
||||
Les délais en terme de jour se calculeront avec <code>utils.age_in_days</code>. Ici
|
||||
le délai est de 31 jours à dater de la soumission d’un formulaire.
|
||||
le délai est de 31 jours à dater de la soumission d'un formulaire.
|
||||
</p>
|
||||
|
||||
<example><code>utils.age_in_days(form_receipt_datetime) >= 31</code></example>
|
||||
|
|
|
@ -14,14 +14,14 @@
|
|||
<title>Variables de substitution</title>
|
||||
|
||||
<p>
|
||||
<app>w.c.s.</app> dispose d’un système générique appelé « variables de
|
||||
<app>w.c.s.</app> dispose d'un système générique appelé « variables de
|
||||
substitutions » qui permet de faire référence à des données internes,
|
||||
provenant du système, d’un formulaire, d’un champ, etc.
|
||||
provenant du système, d'un formulaire, d'un champ, etc.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Ce système est particulièrement utile dans le paramétrage du contenu d’un
|
||||
courriel ou dans la définition d’une logique de traitement d’un formulaire,
|
||||
Ce système est particulièrement utile dans le paramétrage du contenu d'un
|
||||
courriel ou dans la définition d'une logique de traitement d'un formulaire,
|
||||
mais est également accessible depuis le thème, pour le préremplissage de
|
||||
champs, etc.
|
||||
</p>
|
||||
|
@ -40,15 +40,15 @@ champs, etc.
|
|||
</tr>
|
||||
<tr>
|
||||
<td><p><code>site_theme</code></p></td>
|
||||
<td><p>L’identifiant du thème en cours</p></td>
|
||||
<td><p>L'identifiant du thème en cours</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><p><code>site_url</code></p></td>
|
||||
<td><p>L’adresse du site</p></td>
|
||||
<td><p>L'adresse du site</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><p><code>site_url_backoffice</code></p></td>
|
||||
<td><p>L’adresse vers le backoffice du site</p></td>
|
||||
<td><p>L'adresse vers le backoffice du site</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><p><code>site_lang</code></p></td>
|
||||
|
@ -60,15 +60,15 @@ champs, etc.
|
|||
</tr>
|
||||
<tr>
|
||||
<td><p><code>now</code></p></td>
|
||||
<td><p>La date et l’heure du jour</p></td>
|
||||
<td><p>La date et l'heure du jour</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><p><code>session_user_display_name</code></p></td>
|
||||
<td><p>Le nom de l’utilisateur connecté</p></td>
|
||||
<td><p>Le nom de l'utilisateur connecté</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><p><code>session_user_email</code></p></td>
|
||||
<td><p>L’adresse électronique de l’utilisateur connecté</p></td>
|
||||
<td><p>L'adresse électronique de l'utilisateur connecté</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><p><code>is_in_backoffice</code></p></td>
|
||||
|
@ -77,14 +77,14 @@ champs, etc.
|
|||
</table>
|
||||
|
||||
<p>
|
||||
À l’intérieur d’une catégorie, les variables suivantes sont également
|
||||
À l'intérieur d'une catégorie, les variables suivantes sont également
|
||||
définies :
|
||||
</p>
|
||||
|
||||
<table shade="rows">
|
||||
<tr>
|
||||
<td><p><code>category_name</code></p></td>
|
||||
<td><p>L’intitulé de la catégorie</p></td>
|
||||
<td><p>L'intitulé de la catégorie</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><p><code>category_description</code></p></td>
|
||||
|
@ -92,14 +92,14 @@ champs, etc.
|
|||
</tr>
|
||||
<tr>
|
||||
<td><p><code>category_id</code></p></td>
|
||||
<td><p>L’identifiant de la catégorie</p></td>
|
||||
<td><p>L'identifiant de la catégorie</p></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<note><p>
|
||||
Des variables supplémentaires peuvent également être définies par
|
||||
l’administrateur système, via des variables d’environnement ou la
|
||||
configuration de l’instance de w.c.s.
|
||||
l'administrateur système, via des variables d'environnement ou la
|
||||
configuration de l'instance de w.c.s.
|
||||
</p></note>
|
||||
|
||||
</section>
|
||||
|
@ -117,7 +117,7 @@ champs, etc.
|
|||
</tr>
|
||||
<tr>
|
||||
<td><p><code>form_receipt_time</code></p></td>
|
||||
<td><p>La date et l’heure de réception du formulaire</p></td>
|
||||
<td><p>La date et l'heure de réception du formulaire</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><p><code>form_name</code></p></td>
|
||||
|
@ -129,19 +129,19 @@ champs, etc.
|
|||
</tr>
|
||||
<tr>
|
||||
<td><p><code>form_slug</code></p></td>
|
||||
<td><p>Le « slug » (partie d’adresse) du formulaire</p></td>
|
||||
<td><p>Le « slug » (partie d'adresse) du formulaire</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><p><code>form_url</code></p></td>
|
||||
<td><p>L’adresse vers la vue du formulaire</p></td>
|
||||
<td><p>L'adresse vers la vue du formulaire</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><p><code>form_url_backoffice</code></p></td>
|
||||
<td><p>L’adresse vers le formulaire dans le backoffice</p></td>
|
||||
<td><p>L'adresse vers le formulaire dans le backoffice</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><p><code>form_tracking_code</code></p></td>
|
||||
<td><p>Le code de suivi du formulaire, s’il existe</p></td>
|
||||
<td><p>Le code de suivi du formulaire, s'il existe</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><p><code>form_criticality_level</code></p></td>
|
||||
|
@ -151,30 +151,30 @@ champs, etc.
|
|||
</section>
|
||||
|
||||
<section id="user-info">
|
||||
<title>Informations sur l’utilisateur (demandeur)</title>
|
||||
<title>Informations sur l'utilisateur (demandeur)</title>
|
||||
|
||||
<p>
|
||||
Les données contiennent aussi des informations sur l’utilisateur ayant
|
||||
Les données contiennent aussi des informations sur l'utilisateur ayant
|
||||
complété le formulaire.
|
||||
</p>
|
||||
|
||||
<table shade="rows">
|
||||
<tr>
|
||||
<td><p><code>form_user_display_name</code></p></td>
|
||||
<td><p>Le nom de l’utilisateur</p></td>
|
||||
<td><p>Le nom de l'utilisateur</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><p><code>form_user_email</code></p></td>
|
||||
<td><p>L’adresse électronique de l’utilisateur</p></td>
|
||||
<td><p>L'adresse électronique de l'utilisateur</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><p><code>form_user_name_identifier_<var>n</var></code></p></td>
|
||||
<td><p>Identifiant SAML de l’utilisateur (NameID), pour le fournisseur
|
||||
d’identités numéro n (n commençant à 0)</p></td> </tr>
|
||||
<td><p>Identifiant SAML de l'utilisateur (NameID), pour le fournisseur
|
||||
d'identités numéro n (n commençant à 0)</p></td> </tr>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
Si un champ utilisateur personnalisé dispose d’un nom de variable, alors
|
||||
Si un champ utilisateur personnalisé dispose d'un nom de variable, alors
|
||||
il est accessible sous la forme <code>form_user_var_<var>variable du
|
||||
champ</var></code>, par exemple <code>form_user_var_prenom</code>.
|
||||
</p>
|
||||
|
@ -187,12 +187,12 @@ champs, etc.
|
|||
<table shade="rows">
|
||||
<tr>
|
||||
<td><p><code>form_details</code></p></td>
|
||||
<td><p>L’ensemble des champs et de leur valeurs</p></td>
|
||||
<td><p>L'ensemble des champs et de leur valeurs</p></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
Il est également possible au moment de la définition d’un champ, de lui
|
||||
Il est également possible au moment de la définition d'un champ, de lui
|
||||
affecter un nom de variable, cela rendra la valeur associée à ce champ
|
||||
accessible via une variable de la forme <code>form_var_<var>variable du
|
||||
champ</var></code> (par exemple : <code>form_var_courriel</code>).
|
||||
|
@ -202,7 +202,7 @@ champs, etc.
|
|||
Pour les champs de type « Liste » dont les éléments contiendraient à la
|
||||
fois un libellé et un identifiant (ce qui arrive pour les champs alimentés
|
||||
depuis une source de données), le libellé se trouvera dans <code>form_var_<var>variable
|
||||
du champ</var></code> et l’identifiant dans <code>form_var_<var>variable du
|
||||
du champ</var></code> et l'identifiant dans <code>form_var_<var>variable du
|
||||
champ</var>_raw</code>.
|
||||
</p>
|
||||
|
||||
|
@ -274,7 +274,7 @@ champs, etc.
|
|||
</table>
|
||||
|
||||
<p>
|
||||
Si d’autres informations ont été fournies sur le contexte de la saisie
|
||||
Si d'autres informations ont été fournies sur le contexte de la saisie
|
||||
elles sont disponibles dans des variables de la forme
|
||||
<code>form_submission_context_<var>foobar</var></code>.
|
||||
</p>
|
||||
|
@ -287,11 +287,11 @@ champs, etc.
|
|||
<title>Variables de session</title>
|
||||
|
||||
<p>
|
||||
La session de l’usager contient une série d’informations fixes (par exemple
|
||||
La session de l'usager contient une série d'informations fixes (par exemple
|
||||
le <code>session_user_display_name</code> décrit en haut de page), il est
|
||||
aussi possible d’y ajouter de nouvelles données par l’intermédiaire de
|
||||
liens contenant des paramètres. Cela permet par exemple d’inclure une URL
|
||||
personnalisée dans un courriel vers l’usager qui assurera le
|
||||
aussi possible d'y ajouter de nouvelles données par l'intermédiaire de
|
||||
liens contenant des paramètres. Cela permet par exemple d'inclure une URL
|
||||
personnalisée dans un courriel vers l'usager qui assurera le
|
||||
préremplissage automatique de champs.
|
||||
</p>
|
||||
|
||||
|
@ -320,7 +320,7 @@ champs, etc.
|
|||
<note style="important">
|
||||
<p>
|
||||
Ce fonctionnement doit explicitement être autorisé par
|
||||
l’administrateur système, la liste des variables permises doit être ajoutée
|
||||
l'administrateur système, la liste des variables permises doit être ajoutée
|
||||
au fichier <code>site_options.cfg</code>, dans la section
|
||||
<code>[options]</code>, par exemple :
|
||||
</p>
|
||||
|
|
|
@ -13,9 +13,9 @@
|
|||
<title>Mécanique de gabarits</title>
|
||||
|
||||
<p>
|
||||
De nombreux éléments permettent l’utilisation d’un système simple permettant
|
||||
de générer du contenu variant suivant certaines données. L’exemple le plus
|
||||
simple peut être le contenu d’un courriel, dans lequel l’utilisateur se voit
|
||||
De nombreux éléments permettent l'utilisation d'un système simple permettant
|
||||
de générer du contenu variant suivant certaines données. L'exemple le plus
|
||||
simple peut être le contenu d'un courriel, dans lequel l'utilisateur se voit
|
||||
souhaiter la bienvenue.
|
||||
</p>
|
||||
|
||||
|
@ -23,13 +23,13 @@ souhaiter la bienvenue.
|
|||
<code>
|
||||
Bienvenue {{session_user_display_name}},
|
||||
|
||||
Toute l’équipe de {{site_name}} vous remercie de votre inscription
|
||||
Toute l'équipe de {{site_name}} vous remercie de votre inscription
|
||||
et vous souhaite une agréable visite.
|
||||
</code>
|
||||
</example>
|
||||
|
||||
<p>
|
||||
À l’usage, les contenus proposés entre crochets seront substitués, pour donner
|
||||
À l'usage, les contenus proposés entre crochets seront substitués, pour donner
|
||||
le résultat suivant :
|
||||
</p>
|
||||
|
||||
|
@ -37,16 +37,16 @@ le résultat suivant :
|
|||
<code>
|
||||
Bienvenue <var>Lætitia</var>,
|
||||
|
||||
Toute l’équipe de <var>Quizz du jour</var> vous remercie de votre inscription
|
||||
Toute l'équipe de <var>Quizz du jour</var> vous remercie de votre inscription
|
||||
et vous souhaite une agréable visite.
|
||||
</code>
|
||||
</example>
|
||||
|
||||
<p>
|
||||
Il est également possible d’afficher du contenu de manière conditionnelle,
|
||||
Il est également possible d'afficher du contenu de manière conditionnelle,
|
||||
en utilisant la syntaxe <code>{% if variable %}...{% endif %}</code> ou
|
||||
<code>{% if variable %}...{% else %}...{% endif %}</code> pour vérifier la présence
|
||||
d’une valeur dans <var>variable</var>.
|
||||
d'une valeur dans <var>variable</var>.
|
||||
</p>
|
||||
|
||||
<example>
|
||||
|
@ -62,27 +62,27 @@ Pour rappel, voici les renseignements que vous nous avez fournis :
|
|||
</example>
|
||||
|
||||
<p>
|
||||
Pour tester non pas la présence d’une valeur mais le contenu de celle-ci, la
|
||||
Pour tester non pas la présence d'une valeur mais le contenu de celle-ci, la
|
||||
syntaxe est <code>{% if variable == valeur %}...{% endif %}</code>, avec également la
|
||||
possibilité d’un <code>{% else %}</code>.
|
||||
possibilité d'un <code>{% else %}</code>.
|
||||
</p>
|
||||
|
||||
<example>
|
||||
<code>
|
||||
Pour toute information complémentaire, n’hésitez pas à nous contacter au
|
||||
Pour toute information complémentaire, n'hésitez pas à nous contacter au
|
||||
numéro {% if form_var_pays == "France" %}0800 123 456{% else %}+33 1 1234 5678{% endif %}.
|
||||
</code>
|
||||
</example>
|
||||
|
||||
<note><p>
|
||||
Pour plus d’informations la syntaxe utilisée est celle des gabarits Django, il en
|
||||
Pour plus d'informations la syntaxe utilisée est celle des gabarits Django, il en
|
||||
existe une <link href="https://docs.djangoproject.com/fr/1.8/ref/templates/">documentation
|
||||
détaillée</link> en ligne.
|
||||
</p></note>
|
||||
|
||||
<note style="advanced"><p>
|
||||
Précemment un autre langage de description des gabarits était utilisé (EZT),
|
||||
caractérisé par l’utilisation de crochets (ex: <code>[form_var_email]</code>), il
|
||||
caractérisé par l'utilisation de crochets (ex: <code>[form_var_email]</code>), il
|
||||
est toujours disponible mais désormais déconseillé; pour mémoire sa
|
||||
<link href="https://github.com/gstein/ezt/blob/wiki/Syntax.md#directives">référence
|
||||
détaillée</link> (en anglais) est toujours en ligne.
|
||||
|
|
|
@ -11,24 +11,24 @@
|
|||
<desc>Quelques options</desc>
|
||||
</info>
|
||||
|
||||
<title>Variables d’environnement</title>
|
||||
<title>Variables d'environnement</title>
|
||||
|
||||
<p>
|
||||
La majeure partie du paramétrage est accessible via les écrans de paramétrage
|
||||
ou via le fichier <code>site-options.cfg</code>; pour faciliter le travail de
|
||||
configuration quand il s’agit d’informations proches de l’administration
|
||||
système, il existe également la possibilité d’utiliser des variables
|
||||
d’environnement.
|
||||
configuration quand il s'agit d'informations proches de l'administration
|
||||
système, il existe également la possibilité d'utiliser des variables
|
||||
d'environnement.
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<title>Redirection des emails</title>
|
||||
|
||||
<p>
|
||||
Accessible dans l’écran de paramétrage des options de debug, il est
|
||||
Accessible dans l'écran de paramétrage des options de debug, il est
|
||||
aussi possible de forcer les emails générés par la plateforme à être
|
||||
envoyés vers une adresse unique, en positionnant la variable
|
||||
d’environnement <code>QOMMON_MAIL_REDIRECTION</code>.
|
||||
d'environnement <code>QOMMON_MAIL_REDIRECTION</code>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
@ -38,9 +38,9 @@ d’environnement.
|
|||
<p>
|
||||
Par défaut les sauts de workflow sont évalués trois fois par heure, toutes
|
||||
les vingt minutes. Il est possible de définir une autre fréquence dans la
|
||||
variable d’environnement <code>WCS_JUMP_TIMEOUT_CHECKS</code>. Cette
|
||||
variable d'environnement <code>WCS_JUMP_TIMEOUT_CHECKS</code>. Cette
|
||||
variable doit contenir le nombre de vérifications à réaliser par heure, le
|
||||
maximum est d’une vérification toutes les minutes (i.e.
|
||||
maximum est d'une vérification toutes les minutes (i.e.
|
||||
<code>WCS_JUMP_TIMEOUT_CHECKS=60</code>).
|
||||
</p>
|
||||
</section>
|
||||
|
|
|
@ -8,57 +8,64 @@
|
|||
<name>Frédéric Péters</name>
|
||||
<email>fpeters@entrouvert.com</email>
|
||||
</credit>
|
||||
<desc>Autorisations d’accès spécifiques et accès de secours</desc>
|
||||
<desc>Autorisations d'accès spécifiques et accès de secours</desc>
|
||||
</info>
|
||||
|
||||
<title>Permissions d’administration</title>
|
||||
<title>Permissions d'administration</title>
|
||||
|
||||
<p>
|
||||
Dans le fonctionnement de base un compte d’administration ouvre l’accès à
|
||||
toutes les pages de l’interface d’administration, il est néanmoins possible
|
||||
de paramétrer de manière plus fine l’accès aux différentes sections.
|
||||
Dans le fonctionnement de base un compte administrateur ouvre l'accès à
|
||||
toutes les pages de l'interface d'administration, il est néanmoins possible
|
||||
de paramétrer de manière plus fine l'accès aux différentes sections.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Dans l’espace de paramétrage, dans la section « Sécurité », suivez le lien
|
||||
« Permissions d’administration ». Pour chacune des grandes sections de
|
||||
l’administration (<gui>Formulaires</gui>, <gui>Modèles de fiches</gui>,
|
||||
<gui>Workflows</gui>…) vous pouvez restreindre l’accès aux utilisateurs
|
||||
Dans l'espace de paramétrage, dans la section « Sécurité », suivez le lien
|
||||
« Permissions d'administration ». Pour chacune des grandes sections de
|
||||
l'administration (<gui>Formulaires</gui>, <gui>Workflows</gui>,
|
||||
<gui>Utilisateurs</gui>…) vous pouvez restreindre l'accès aux utilisateurs
|
||||
disposant de rôles particuliers.
|
||||
</p>
|
||||
|
||||
<note style="info">
|
||||
<p>
|
||||
Disposer du rôle n'est pas suffisant, il reste nécessaire aux utilisateurs
|
||||
concernés d'avoir « Compte administrateur » coché dans leur profil.
|
||||
</p>
|
||||
</note>
|
||||
|
||||
<section id="failsafe">
|
||||
<title>Accès d’administration de secours</title>
|
||||
<title>Accès administrateur de secours</title>
|
||||
|
||||
<p>
|
||||
En cas de mauvaise manipulation et de perte totale de l’accès à l’interface
|
||||
d’administration, le système dispose d’un moyen de secours
|
||||
pour temporairement désactiver la vérification des permissions d’accès.
|
||||
En cas de mauvaise manipulation et de perte totale de l'accès à l'interface
|
||||
d'administration, l'administrateur système dispose d'un moyen de secours
|
||||
pour temporairement désactiver la vérification des permissions d'accès.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Dans le répertoire de l’instance (<file>/var/lib/wcs/tenants/www.example.net/</file>
|
||||
Dans le répertoire de l'instance (<file>/var/lib/wcs/www.example.net/</file>
|
||||
par exemple), un fichier <file>ADMIN_FOR_ALL</file> doit être créé,
|
||||
contenant l’adresse IP qui sera utilisée pour la connexion.
|
||||
contenant l'adresse IP qui sera utilisée pour la connexion.
|
||||
</p>
|
||||
|
||||
<screen>
|
||||
<output style="prompt"># </output><input>cd /var/lib/wcs/tenants/www.example.net/</input>
|
||||
<output style="prompt"># </output><input>cd /var/lib/wcs/www.example.net/</input>
|
||||
<output style="prompt"># </output><input>echo 77.109.103.99 > ADMIN_FOR_ALL</input>
|
||||
</screen>
|
||||
|
||||
<p>
|
||||
L’interface d’administration devient alors accessible pour permettre la
|
||||
correction de l’éventuelle erreur. Dans ce mode elle affiche son bandeau
|
||||
L'interface d'administration devient alors accessible pour permettre la
|
||||
correction de l'éventuelle erreur. Dans ce mode elle affiche son bandeau
|
||||
en rouge vif, rappel que celui-ci est exceptionnel et dangereux. Dès
|
||||
l’accès restauré, il est important de supprimer le fichier
|
||||
l'accès restauré, il est important de supprimer le fichier
|
||||
<file>ADMIN_FOR_ALL</file>.
|
||||
</p>
|
||||
|
||||
<note style="warning">
|
||||
<p>
|
||||
Pour des raisons de compatibilité, un fichier <file>ADMIN_FOR_ALL</file>
|
||||
vide ouvre l’accès pour toutes les connexions; ce comportement
|
||||
vide ouvre l'accès pour toutes les connexions; ce comportement
|
||||
dangereux sera supprimé dans une version à venir, son utilisation est
|
||||
fortement découragée.
|
||||
</p>
|
||||
|
|
|
@ -14,18 +14,18 @@
|
|||
<title>Anonymisation</title>
|
||||
|
||||
<p>
|
||||
Dans le circuit de traitement d’une demande, après qu’elle ait été traitée,
|
||||
il peut être souhaité d’en conserver une trace à des fins statistiques, tout en
|
||||
Dans le circuit de traitement d'une demande, après qu'elle ait été traitée,
|
||||
il peut être souhaité d'en conserver une trace à des fins statistiques, tout en
|
||||
lui retirant toute information à caractère personnel.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
L’élément « Anonymisation » répond à ce genre de besoin; il sera généralement
|
||||
placé dans un état du workflow atteint après l’expiration d’un délai.
|
||||
L'élément « Anonymisation » répond à ce genre de besoin; il sera généralement
|
||||
placé dans un état du workflow atteint après l'expiration d'un délai.
|
||||
</p>
|
||||
|
||||
<note style="important"><p>
|
||||
L’anonymisation des données privées est une obligation légale dans beaucoup de
|
||||
L'anonymisation des données privées est une obligation légale dans beaucoup de
|
||||
situations.
|
||||
</p></note>
|
||||
|
||||
|
|
|
@ -12,25 +12,25 @@
|
|||
|
||||
</info>
|
||||
|
||||
<title>Affichage d’un formulaire</title>
|
||||
<title>Affichage d'un formulaire</title>
|
||||
|
||||
<p>
|
||||
Lors du traitement d’une demande il peut être nécessaire de demander des
|
||||
informations supplémentaires à l’usager, ou qu’un agent complète la demande
|
||||
avec des informations internes (à l’image d’un « cadre réservé à
|
||||
l’administration » sur du papier).
|
||||
Lors du traitement d'une demande il peut être nécessaire de demander des
|
||||
informations supplémentaires à l'usager, ou qu'un agent complète la demande
|
||||
avec des informations internes (à l'image d'un « cadre réservé à
|
||||
l'administration » sur du papier).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
L’élément « Afficher un formulaire » répond à ce genre de besoin, en
|
||||
permettant la définition d’un formulaire de renseignement d’informations
|
||||
L'élément « Afficher un formulaire » répond à ce genre de besoin, en
|
||||
permettant la définition d'un formulaire de renseignement d'informations
|
||||
supplémentaires.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
L’interface pour définir les champs est identique à celle utilisée pour définir
|
||||
L'interface pour définir les champs est identique à celle utilisée pour définir
|
||||
les formulaires généraux. Elle est accessible en cliquant sur le lien <gui>Éditer
|
||||
les champs</gui>. Toutes les options sont disponibles, à l’exception du
|
||||
les champs</gui>. Toutes les options sont disponibles, à l'exception du
|
||||
multipage.
|
||||
</p>
|
||||
|
||||
|
@ -42,7 +42,7 @@ que la valeur qui sera renseignée par le champ soit sauvegardée.
|
|||
<p>
|
||||
Il est nécessaire également de définir à qui sera présenté le formulaire, via
|
||||
le champ <gui>À</gui>, et de lui attribuer un <gui>Nom de variable</gui> qui
|
||||
permettra aux valeurs sauvegardées d’être accessibles au niveau des <link
|
||||
permettra aux valeurs sauvegardées d'être accessibles au niveau des <link
|
||||
xref="misc-substvars">variables de substitution</link>, sous la forme
|
||||
<code><var>variable du formulaire</var>_var_<var>variable du
|
||||
champ</var></code> (par exemple : <code>contact_interne_var_telephone</code>).
|
||||
|
@ -50,7 +50,7 @@ champ</var></code> (par exemple : <code>contact_interne_var_telephone</code>).
|
|||
|
||||
<note><p>
|
||||
Pour les champs de type fichier, la variable contiendra le nom du fichier.
|
||||
L’adresse du fichier sera présente dans la variable nommée
|
||||
L'adresse du fichier sera présente dans la variable nommée
|
||||
<code><var>variable du formulaire</var>_var_<var>variable du champ</var>_url</code>.
|
||||
</p></note>
|
||||
|
||||
|
|
|
@ -15,13 +15,13 @@
|
|||
|
||||
<p>
|
||||
Une fois la géolocalisation activée pour un formulaire, le workflow associé
|
||||
peut faire appel à l’action de géolocalisation pour attacher des coordonnées
|
||||
peut faire appel à l'action de géolocalisation pour attacher des coordonnées
|
||||
géographiques à la demande.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Ces coordonnées peuvent être obtenues par géocodage à partir d’une adresse ou
|
||||
en les extrayant d’un champ « Carte » ou des métadonnées attachées à une
|
||||
Ces coordonnées peuvent être obtenues par géocodage à partir d'une adresse ou
|
||||
en les extrayant d'un champ « Carte » ou des métadonnées attachées à une
|
||||
photographie qui aurait été transférée via un champ de type « Fichier ».
|
||||
</p>
|
||||
|
||||
|
@ -32,13 +32,13 @@ paramétrant pour ne pas écraser des coordonnées précédemment acquises.
|
|||
</p>
|
||||
|
||||
<p>
|
||||
Une fois le géocodage réussi, l’information est mise à disposition dans les
|
||||
Une fois le géocodage réussi, l'information est mise à disposition dans les
|
||||
variables <code>form_geoloc_base_lat</code> pour la latitude et
|
||||
<code>form_geoloc_base_lon</code> pour la longitude.
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<title>Géocodage à partir d’une adresse</title>
|
||||
<title>Géocodage à partir d'une adresse</title>
|
||||
|
||||
<p>
|
||||
Le paramétrage se fait en renseignant une chaîne de caractère produisant une
|
||||
|
@ -50,10 +50,10 @@ variables <code>form_geoloc_base_lat</code> pour la latitude et
|
|||
</section>
|
||||
|
||||
<section>
|
||||
<title>Extraction d’un champ « Carte »</title>
|
||||
<title>Extraction d'un champ « Carte »</title>
|
||||
|
||||
<p>
|
||||
Le paramètre est une expression faisant référence à une variable tirée d’un
|
||||
Le paramètre est une expression faisant référence à une variable tirée d'un
|
||||
champ « Carte ».
|
||||
</p>
|
||||
|
||||
|
@ -61,10 +61,10 @@ variables <code>form_geoloc_base_lat</code> pour la latitude et
|
|||
</section>
|
||||
|
||||
<section>
|
||||
<title>Extraction d’une photographie</title>
|
||||
<title>Extraction d'une photographie</title>
|
||||
|
||||
<p>
|
||||
Le paramètre est une expression pointant une variable tirée d’un champ de
|
||||
Le paramètre est une expression pointant une variable tirée d'un champ de
|
||||
type « Fichier »; le fichier ainsi pointé doit être une image contenant des
|
||||
métadonnées EXIF, renseignant la localisation de la prise de vue.
|
||||
</p>
|
||||
|
|
|
@ -14,15 +14,15 @@
|
|||
<title>Changement de statut automatique</title>
|
||||
|
||||
<p>
|
||||
L’action de changement de statut automatique permet de passer automatiquement
|
||||
un formulaire d’un statut à un autre, avec la possibilité de définir les
|
||||
L'action de changement de statut automatique permet de passer automatiquement
|
||||
un formulaire d'un statut à un autre, avec la possibilité de définir les
|
||||
critères à rencontrer pour que la transition ait lieu.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Ces critères sont de trois ordres : une condition particulière, pouvant par
|
||||
exemple porter sur des données du formulaire, un déclencheur externe, pour
|
||||
l’interaction avec des systèmes externes, et un délai d’expiration, pour
|
||||
l'interaction avec des systèmes externes, et un délai d'expiration, pour
|
||||
assurer une transition automatique après un temps donné.
|
||||
</p>
|
||||
|
||||
|
@ -47,7 +47,7 @@ tous être remplis pour que la transition ait lieu.
|
|||
|
||||
<p>
|
||||
Ce dispositif permet à un système tiers de provoquer la transition de statut,
|
||||
il est décrit dans la documentation sur l’API, dans la page <link xref="api-workflow"/>.
|
||||
il est décrit dans la documentation sur l'API, dans la page <link xref="api-workflow"/>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
@ -55,7 +55,7 @@ tous être remplis pour que la transition ait lieu.
|
|||
<title>Expiration</title>
|
||||
|
||||
<p>
|
||||
Le critère d’expiration permet d’avoir une transition de statut après un
|
||||
Le critère d'expiration permet d'avoir une transition de statut après un
|
||||
certain délai seulement; il est par exemple utile pour créer un système de
|
||||
relance automatique.
|
||||
</p>
|
||||
|
@ -66,8 +66,8 @@ tous être remplis pour que la transition ait lieu.
|
|||
combiner les unités de temps, par exemple : <code>1 mois 10 jours</code>.
|
||||
</p>
|
||||
<p>
|
||||
Il peut également être spécifié sous forme d’expression Python, en préfixant
|
||||
celle-ci d’un signe =, la valeur doit alors être un nombre de secondes.
|
||||
Il peut également être spécifié sous forme d'expression Python, en préfixant
|
||||
celle-ci d'un signe =, la valeur doit alors être un nombre de secondes.
|
||||
</p>
|
||||
</note>
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<title>Variables de workflow</title>
|
||||
|
||||
<p>
|
||||
Il arrive souvent qu’un même traitement doive être appliqué à différents
|
||||
Il arrive souvent qu'un même traitement doive être appliqué à différents
|
||||
formulaires, à un petit détail près, par exemple le document généré doit être
|
||||
basé sur un modèle différent, un webservice externe doit être appelé avec une
|
||||
autre donnée, etc.
|
||||
|
@ -22,16 +22,16 @@ autre donnée, etc.
|
|||
|
||||
<p>
|
||||
Dans ces situations il est bien sûr possible de dupliquer le workflow autant de
|
||||
fois qu’il existe de variations mais ça entraîne rapidement un coût d’entretien
|
||||
fois qu'il existe de variations mais ça entraîne rapidement un coût d'entretien
|
||||
trop élevé. Les variables de workflow sont une réponse à ce problème, elles
|
||||
permettent de déléguer certains éléments du paramétrage d’un workflow aux
|
||||
permettent de déléguer certains éléments du paramétrage d'un workflow aux
|
||||
formulaires associés.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Pratiquement, la définition des variables se rapproche de la définition des
|
||||
formulaires destinés aux usagers, il s’agit de définir une série de champs. Il
|
||||
y a quand même une différence, lors de la définition d’un champ celui-ci doit
|
||||
formulaires destinés aux usagers, il s'agit de définir une série de champs. Il
|
||||
y a quand même une différence, lors de la définition d'un champ celui-ci doit
|
||||
être associé soit à un nom de variable, qui pourra alors être utilisé dans le
|
||||
formulaire, soit directement à un élément du workflow, qui sera alors substitué
|
||||
automatiquement. Les deux usages sont décrits par la suite.
|
||||
|
@ -57,8 +57,8 @@ cliquer dessus ouvre une fenêtre avec la liste des paramètres à remplir.
|
|||
<p>
|
||||
Un workflow de concours pourrait ainsi avoir comme variable la
|
||||
description du prix (nommée <code>description_prix</code>), dans la
|
||||
définition d’un formulaire la variable serait remplie avec "Deux
|
||||
places de cinéma" et dans le traitement, une action d’envoi de courriel
|
||||
définition d'un formulaire la variable serait remplie avec "Deux
|
||||
places de cinéma" et dans le traitement, une action d'envoi de courriel
|
||||
pourrait décrire celui-ci ainsi :
|
||||
</p>
|
||||
|
||||
|
@ -71,8 +71,8 @@ tirage au sort.
|
|||
</example>
|
||||
|
||||
<p>
|
||||
Une autre utilisation pourrait être d’avoir une liste à choix multiple comme
|
||||
option de workflow, reprenant par exemple les types d’envoi possibles
|
||||
Une autre utilisation pourrait être d'avoir une liste à choix multiple comme
|
||||
option de workflow, reprenant par exemple les types d'envoi possibles
|
||||
(courrier standard, recommandé, recommandé avec accusé de réception…) (sous
|
||||
le nom <code>mode_envoi</code>); du coté des formulaires il pourrait y avoir
|
||||
un champ de type « Liste » qui serait rempli des éléments qui auraient été
|
||||
|
@ -86,11 +86,11 @@ tirage au sort.
|
|||
<title>Option substituant un élément de workflow</title>
|
||||
|
||||
<p>
|
||||
Dans une variation de l’exemple précédent du coucours, l’entièreté du
|
||||
contenu du courriel pourrait relever d’une option; dans cette situation,
|
||||
Dans une variation de l'exemple précédent du coucours, l'entièreté du
|
||||
contenu du courriel pourrait relever d'une option; dans cette situation,
|
||||
plutôt que définir du côté du courriel que son contenu serait
|
||||
<code>[form_option_contenu_courriel]</code>, il est possible de directement
|
||||
associer l’option de workflow à l’élément d’envoi de courriel.
|
||||
associer l'option de workflow à l'élément d'envoi de courriel.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
|
|
@ -14,12 +14,12 @@
|
|||
<title>Appel à un webservice</title>
|
||||
|
||||
<p>
|
||||
Cette action permet d’appeler un système tiers et d’éventuellement lui
|
||||
Cette action permet d'appeler un système tiers et d'éventuellement lui
|
||||
transmettre des données, dont celles du formulaire en cours.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Le champ URL est obligatoire, il doit contenir l’adresse qui sera appelée,
|
||||
Le champ URL est obligatoire, il doit contenir l'adresse qui sera appelée,
|
||||
celle-ci peut contenir des <link xref="misc-substvars">variables</link>, pour
|
||||
par exemple transmettre une information particulière.
|
||||
</p>
|
||||
|
@ -30,24 +30,24 @@ par exemple transmettre une information particulière.
|
|||
|
||||
<p>
|
||||
Le tableau « Données à envoyer en paramètre » permet de décrire des données qui
|
||||
seront transmises sous la forme de paramètres d’URL. Sur chaque ligne, la
|
||||
seront transmises sous la forme de paramètres d'URL. Sur chaque ligne, la
|
||||
colonne de gauche est le nom de la clé, celle de droite la valeur. La valeur
|
||||
peut être une expression Python, pour cela elle doit commencer par le signe
|
||||
« = ». Les paramètres d’URL ne peuvent être que des chaînes, si ce n’est pas le
|
||||
« = ». Les paramètres d'URL ne peuvent être que des chaînes, si ce n'est pas le
|
||||
cas la donnée sera transformée en chaîne de force.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
La case à cocher « Envoyer le formulaire (POST, en JSON) » indique que
|
||||
l’ensemble des données du formulaire doivent être transmises, avec un appel de
|
||||
l'ensemble des données du formulaire doivent être transmises, avec un appel de
|
||||
type <code>POST</code>, dont le contenu correspondra au formulaire encodé au
|
||||
format JSON, comme décrit dans cette <link xref="api-get#pull">page sur
|
||||
l’API</link>.
|
||||
l'API</link>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Le tableau « Données à envoyer en POST » permet de décrire des données qui
|
||||
seront transmises sous la forme d’un dictionnaire clé-valeur au format JSON.
|
||||
seront transmises sous la forme d'un dictionnaire clé-valeur au format JSON.
|
||||
Sur chaque ligne, la colonne de gauche est le nom de la clé, celle de droite la
|
||||
valeur. La valeur peut être une expression Python, pour cela elle doit
|
||||
commencer par le signe « = ».
|
||||
|
@ -84,14 +84,14 @@ commencer par le signe « = ».
|
|||
dans le JSON du formulaire, dans une clé « extra ».
|
||||
</p></item>
|
||||
<item><p>
|
||||
Si aucune donnée n’est indiquée et que le formulaire ne doit pas être transmis,
|
||||
alors la requête HTTP effectuée est un GET sur l’URL.
|
||||
Si aucune donnée n'est indiquée et que le formulaire ne doit pas être transmis,
|
||||
alors la requête HTTP effectuée est un GET sur l'URL.
|
||||
</p></item>
|
||||
</list>
|
||||
</note>
|
||||
|
||||
<p>
|
||||
Le paramètre « Nom de variable » permet d’enregistrer le résultat retourné
|
||||
Le paramètre « Nom de variable » permet d'enregistrer le résultat retourné
|
||||
par le webservice, le retour HTTP de celui-ci sera enregistré dans
|
||||
<code><var>variable</var>_status</code> (voir plus loin, le traitement des
|
||||
erreurs) et le contenu même de la réponse, si elle est au format JSON,
|
||||
|
@ -100,7 +100,7 @@ sera enregistré dans <code><var>variable</var>_response</code>.
|
|||
|
||||
<p>
|
||||
Le paramètre « Clé de signature de la requête » permet de signer la requête
|
||||
avant de l’envoyer au webservice, avec la valeur du champ comme clé de
|
||||
avant de l'envoyer au webservice, avec la valeur du champ comme clé de
|
||||
signature.
|
||||
</p>
|
||||
|
||||
|
@ -109,13 +109,13 @@ signature.
|
|||
|
||||
<p>
|
||||
En précisant un nom de variable (exemple : <code>webservice</code>), il est
|
||||
possible de placer derrière l’appel au webservice une action de <link
|
||||
possible de placer derrière l'appel au webservice une action de <link
|
||||
xref="wf-jump">changement de statut automatique</link> faisant référence
|
||||
à la variable.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Par exemple, pour s’assurer que le retour fait par le webservice était bien
|
||||
Par exemple, pour s'assurer que le retour fait par le webservice était bien
|
||||
un code HTTP 200 et que le contenu de la réponse contenait bien un
|
||||
dictionnaire <code>data</code> dont la clé <code>result</code> valait
|
||||
<code>OK</code> :
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
for DIRECTORY in "htmlcov" "venv"
|
||||
do
|
||||
if [ -d "$DIRECTORY" ]; then
|
||||
rm -r $DIRECTORY
|
||||
fi
|
||||
done
|
||||
|
||||
virtualenv --system-site-packages venv
|
||||
PIP_BIN=venv/bin/pip
|
||||
|
||||
rm -f coverage.xml
|
||||
rm -f test_results.xml
|
||||
cat << _EOF_ > .coveragerc
|
||||
[run]
|
||||
omit = wcs/qommon/vendor/*.py
|
||||
|
||||
[report]
|
||||
omit = wcs/qommon/vendor/*.py
|
||||
_EOF_
|
||||
|
||||
# $PIP_BIN install --upgrade 'pip<8'
|
||||
$PIP_BIN install --upgrade setuptools
|
||||
$PIP_BIN install --upgrade 'pytest<4.1' 'attrs<19.2' WebTest mock pytest-cov pyquery pytest-django
|
||||
$PIP_BIN install --upgrade 'pylint<1.8' # 1.8 broken (cf build #3023)
|
||||
$PIP_BIN install git+https://git.entrouvert.org/debian/django-ckeditor.git
|
||||
$PIP_BIN install --upgrade 'Django<1.12' 'gadjo' 'pyproj' 'django-ratelimit<3'
|
||||
|
||||
DJANGO_SETTINGS_MODULE=wcs.settings \
|
||||
WCS_SETTINGS_FILE=tests/settings.py \
|
||||
LC_ALL=C LC_TIME=C LANG=C \
|
||||
PYTHONPATH=$(pwd):$PYTHONPATH venv/bin/py.test --junitxml=test_results.xml --cov-report xml --cov-report html --cov=wcs/ --cov-config .coveragerc -v tests/
|
||||
test -f pylint.out && cp pylint.out pylint.out.prev
|
||||
(venv/bin/pylint -f parseable --rcfile /var/lib/jenkins/pylint.wcs.rc wcs | tee pylint.out) || /bin/true
|
||||
test -f pylint.out.prev && (diff pylint.out.prev pylint.out | grep '^[><]' | grep .py) || /bin/true
|
|
@ -1,9 +1,9 @@
|
|||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == '__main__':
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wcs.settings')
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "wcs.settings")
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
|
|
122
pylint.rc
|
@ -1,122 +0,0 @@
|
|||
[MASTER]
|
||||
persistent=yes
|
||||
ignore=vendor,Bouncers,ezt.py
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=
|
||||
abstract-method,
|
||||
arguments-differ,
|
||||
attribute-defined-outside-init,
|
||||
broad-except,
|
||||
broad-exception-raised,
|
||||
consider-using-dict-comprehension,
|
||||
consider-using-f-string,
|
||||
consider-using-set-comprehension,
|
||||
cyclic-import,
|
||||
duplicate-code,
|
||||
fixme,
|
||||
global-variable-undefined,
|
||||
import-outside-toplevel,
|
||||
inconsistent-return-statements,
|
||||
invalid-name,
|
||||
keyword-arg-before-vararg,
|
||||
missing-class-docstring,
|
||||
missing-function-docstring,
|
||||
missing-module-docstring,
|
||||
no-else-return,
|
||||
no-member,
|
||||
non-parent-init-called,
|
||||
not-callable,
|
||||
possibly-unused-variable,
|
||||
protected-access,
|
||||
raise-missing-from,
|
||||
redefined-argument-from-local,
|
||||
redefined-builtin,
|
||||
redefined-outer-name,
|
||||
signature-differs,
|
||||
stop-iteration-return,
|
||||
super-init-not-called,
|
||||
superfluous-parens,
|
||||
too-many-branches,
|
||||
too-many-instance-attributes,
|
||||
too-many-lines,
|
||||
too-many-locals,
|
||||
too-many-nested-blocks,
|
||||
too-many-return-statements,
|
||||
too-many-statements,
|
||||
undefined-loop-variable,
|
||||
unnecessary-comprehension,
|
||||
unnecessary-lambda-assignment,
|
||||
unspecified-encoding,
|
||||
unsubscriptable-object,
|
||||
unsupported-membership-test,
|
||||
unused-argument,
|
||||
use-implicit-booleaness-not-comparison
|
||||
|
||||
|
||||
[TESTOPTIONS]
|
||||
ignored-parents=wcs.qommon.TenantAwareThread
|
||||
|
||||
|
||||
[REPORTS]
|
||||
output-format=parseable
|
||||
|
||||
|
||||
[BASIC]
|
||||
no-docstring-rgx=__.*__|_.*
|
||||
class-rgx=[A-Z_][a-zA-Z0-9_]+$
|
||||
function-rgx=[a-zA_][a-zA-Z0-9_]{2,70}$
|
||||
method-rgx=[a-z_][a-zA-Z0-9_]{2,70}$
|
||||
const-rgx=(([A-Z_][A-Z0-9_]*)|([a-z_][a-z0-9_]*)|(__.*__)|register|urlpatterns)$
|
||||
good-names=_,i,j,k,e,x,Run,,setUp,tearDown,r,p,s,v,fd
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# List of classes names for which member attributes should not be checked
|
||||
# (useful for classes with attributes dynamically set).
|
||||
ignored-classes=SQLObject,WSGIRequest,Publisher,NullSessionManager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E0201 when accessed.
|
||||
generated-members=objects,DoesNotExist,id,pk,_meta,base_fields,context
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes
|
||||
defining-attr-methods=__init__,__new__,setUp
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
init-import=no
|
||||
dummy-variables-rgx=_|dummy|i
|
||||
additional-builtins=_,N_,ngettext
|
||||
good-names=_,i,j,k,e,x,Run,,setUp,tearDown,r,p,s,v,fd
|
||||
|
||||
[SIMILARITIES]
|
||||
min-similarity-lines=6
|
||||
ignore-comments=yes
|
||||
ignore-docstrings=yes
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
notes=FIXME,XXX,TODO
|
||||
|
||||
|
||||
[FORMAT]
|
||||
max-line-length=160
|
||||
max-module-lines=2000
|
||||
indent-string=' '
|
||||
|
||||
|
||||
[DESIGN]
|
||||
max-args=10
|
||||
max-locals=15
|
||||
max-returns=6
|
||||
max-branches=12
|
||||
max-statements=50
|
||||
max-parents=7
|
||||
max-attributes=7
|
||||
min-public-methods=0
|
||||
max-public-methods=50
|
15
pylint.sh
|
@ -1,4 +1,15 @@
|
|||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
set -e -x
|
||||
env
|
||||
pylint --jobs ${NUMPROCESSES:-1} -f parseable --rcfile pylint.rc "$@" | tee pylint.out; test $PIPESTATUS -eq 0
|
||||
if [ -f /var/lib/jenkins/pylint.wcs.rc ]; then
|
||||
PYLINT_RC=/var/lib/jenkins/pylint.wcs.rc
|
||||
elif [ -f pylint.wcs.rc ]; then
|
||||
PYLINT_RC=pylint.wcs.rc
|
||||
else
|
||||
echo No pylint RC found
|
||||
exit 0
|
||||
fi
|
||||
|
||||
test -f pylint.out && cp pylint.out pylint.out.prev
|
||||
pylint -f parseable --rcfile ${PYLINT_RC} "$@" | tee pylint.out || /bin/true
|
||||
test -f pylint.out.prev && (diff pylint.out.prev pylint.out | grep '^[><]' | grep .py) || /bin/true
|
||||
|
|
176
setup.py
|
@ -1,26 +1,18 @@
|
|||
#! /usr/bin/env python3
|
||||
#! /usr/bin/env python
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
try:
|
||||
from setuptools import Command
|
||||
from setuptools.command.build import build as _build
|
||||
from setuptools.errors import CompileError
|
||||
except ImportError:
|
||||
from distutils.cmd import Command
|
||||
from distutils.command.build import build as _build
|
||||
from distutils.errors import CompileError
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
from distutils.cmd import Command
|
||||
from distutils.command.build import build as _build
|
||||
from distutils.command.sdist import sdist
|
||||
from setuptools.command.install_lib import install_lib as _install_lib
|
||||
from setuptools.command.sdist import sdist as _sdist
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
local_cfg = None
|
||||
if os.path.exists('wcs/wcs_cfg.py'):
|
||||
local_cfg = open('wcs/wcs_cfg.py').read()
|
||||
local_cfg = file('wcs/wcs_cfg.py').read()
|
||||
os.unlink('wcs/wcs_cfg.py')
|
||||
|
||||
|
||||
|
@ -36,9 +28,7 @@ class compile_translations(Command):
|
|||
|
||||
def run(self):
|
||||
try:
|
||||
os.environ.pop('DJANGO_SETTINGS_MODULE', None)
|
||||
from django.core.management import call_command
|
||||
|
||||
for path, dirs, files in os.walk('wcs'):
|
||||
if 'locale' not in dirs:
|
||||
continue
|
||||
|
@ -50,39 +40,8 @@ class compile_translations(Command):
|
|||
sys.stderr.write('!!! Please install Django >= 1.4 to build translations\n')
|
||||
|
||||
|
||||
class compile_scss(Command):
|
||||
description = 'compile scss files into css files'
|
||||
user_options = []
|
||||
|
||||
def initialize_options(self):
|
||||
pass
|
||||
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
sass_bin = shutil.which('sassc')
|
||||
if not sass_bin:
|
||||
raise CompileError('sassc is required but was not found.')
|
||||
|
||||
for path, dirnames, filenames in os.walk('wcs'):
|
||||
for filename in filenames:
|
||||
if not filename.endswith('.scss'):
|
||||
continue
|
||||
if filename.startswith('_'):
|
||||
continue
|
||||
subprocess.check_call(
|
||||
[
|
||||
sass_bin,
|
||||
'--sourcemap',
|
||||
'%s/%s' % (path, filename),
|
||||
'%s/%s' % (path, filename.replace('.scss', '.css')),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class build(_build):
|
||||
sub_commands = [('compile_translations', None), ('compile_scss', None)] + _build.sub_commands
|
||||
sub_commands = [('compile_translations', None)] + _build.sub_commands
|
||||
|
||||
|
||||
class install_lib(_install_lib):
|
||||
|
@ -91,7 +50,7 @@ class install_lib(_install_lib):
|
|||
_install_lib.run(self)
|
||||
|
||||
|
||||
class eo_sdist(_sdist):
|
||||
class eo_sdist(sdist):
|
||||
def run(self):
|
||||
if os.path.exists('VERSION'):
|
||||
os.remove('VERSION')
|
||||
|
@ -99,52 +58,34 @@ class eo_sdist(_sdist):
|
|||
version_file = open('VERSION', 'w')
|
||||
version_file.write(version)
|
||||
version_file.close()
|
||||
_sdist.run(self)
|
||||
sdist.run(self)
|
||||
if os.path.exists('VERSION'):
|
||||
os.remove('VERSION')
|
||||
|
||||
|
||||
def data_tree(destdir, sourcedir):
|
||||
extensions = [
|
||||
'.css',
|
||||
'.png',
|
||||
'.jpeg',
|
||||
'.jpg',
|
||||
'.gif',
|
||||
'.xml',
|
||||
'.html',
|
||||
'.js',
|
||||
'.ezt',
|
||||
'.dat',
|
||||
'.eot',
|
||||
'.svg',
|
||||
'.ttf',
|
||||
'.woff',
|
||||
'.scss',
|
||||
'.map',
|
||||
]
|
||||
extensions = ['.css', '.png', '.jpeg', '.jpg', '.gif', '.xml', '.html',
|
||||
'.js', '.ezt', '.dat', '.eot', '.svg', '.ttf', '.woff']
|
||||
r = []
|
||||
for root, dirs, files in os.walk(sourcedir):
|
||||
l = [os.path.join(root, x) for x in files if os.path.splitext(x)[1] in extensions]
|
||||
r.append((root.replace(sourcedir, destdir, 1), l))
|
||||
r.append( (root.replace(sourcedir, destdir, 1), l) )
|
||||
for vcs_dirname in ('CVS', '.svn', '.bzr', '.git'):
|
||||
if vcs_dirname in dirs:
|
||||
dirs.remove(vcs_dirname)
|
||||
return r
|
||||
|
||||
|
||||
def get_version():
|
||||
"""Use the VERSION, if absent generates a version with git describe, if not
|
||||
tag exists, take 0.0- and add the length of the commit log.
|
||||
"""
|
||||
'''Use the VERSION, if absent generates a version with git describe, if not
|
||||
tag exists, take 0.0- and add the length of the commit log.
|
||||
'''
|
||||
if os.path.exists('VERSION'):
|
||||
with open('VERSION') as v:
|
||||
with open('VERSION', 'r') as v:
|
||||
return v.read()
|
||||
if os.path.exists('.git'):
|
||||
p = subprocess.Popen(
|
||||
['git', 'describe', '--dirty=.dirty', '--match=v*'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
result = p.communicate()[0]
|
||||
if p.returncode == 0:
|
||||
|
@ -153,70 +94,47 @@ def get_version():
|
|||
real_number, commit_count, commit_hash = result.split('-', 2)
|
||||
version = '%s.post%s+%s' % (real_number, commit_count, commit_hash)
|
||||
else:
|
||||
version = result.replace('.dirty', '+dirty')
|
||||
version = result
|
||||
return version
|
||||
else:
|
||||
return '0.0.post%s' % len(subprocess.check_output(['git', 'rev-list', 'HEAD']).splitlines())
|
||||
return '0.0.post%s' % len(
|
||||
subprocess.check_output(
|
||||
['git', 'rev-list', 'HEAD']).splitlines())
|
||||
return '0.0'
|
||||
|
||||
|
||||
|
||||
cmdclass = {
|
||||
'build': build,
|
||||
'compile_scss': compile_scss,
|
||||
'compile_translations': compile_translations,
|
||||
'install_lib': install_lib,
|
||||
'sdist': eo_sdist,
|
||||
'sdist': eo_sdist
|
||||
}
|
||||
|
||||
setup(
|
||||
name='wcs',
|
||||
version=get_version(),
|
||||
maintainer='Frederic Peters',
|
||||
maintainer_email='fpeters@entrouvert.com',
|
||||
url='http://wcs.labs.libre-entreprise.org',
|
||||
install_requires=[
|
||||
'Quixote>=3.0,<3.2',
|
||||
'django>=3.2',
|
||||
'psycopg2',
|
||||
'bleach',
|
||||
'dnspython',
|
||||
'gadjo>=0.53',
|
||||
'django-ckeditor<4.5.4',
|
||||
'django-ratelimit<3',
|
||||
'XStatic-Leaflet',
|
||||
'XStatic-Leaflet-GestureHandling',
|
||||
'XStatic-Select2',
|
||||
'pyproj',
|
||||
'pyquery',
|
||||
'unidecode',
|
||||
'lxml',
|
||||
'vobject',
|
||||
'qrcode',
|
||||
'Pillow',
|
||||
'gadjo',
|
||||
'docutils',
|
||||
'django-ckeditor@git+https://git.entrouvert.org/debian/django-ckeditor.git',
|
||||
'XStatic-godo@git+https://git.entrouvert.org/godo.js.git',
|
||||
'langdetect',
|
||||
'python-magic',
|
||||
'workalendar',
|
||||
'requests',
|
||||
'setproctitle',
|
||||
'phonenumbers',
|
||||
'emoji',
|
||||
'psutil',
|
||||
'freezegun',
|
||||
],
|
||||
package_dir={'wcs': 'wcs'},
|
||||
packages=find_packages(),
|
||||
cmdclass=cmdclass,
|
||||
scripts=['manage.py'],
|
||||
include_package_data=True,
|
||||
data_files=data_tree('share/wcs/web/', 'data/web/')
|
||||
+ data_tree('share/wcs/themes/', 'data/themes/')
|
||||
+ data_tree('share/wcs/vendor/', 'data/vendor/')
|
||||
+ data_tree('share/wcs/qommon/', 'wcs/qommon/static/'),
|
||||
)
|
||||
name = 'wcs',
|
||||
version = get_version(),
|
||||
maintainer = "Frederic Peters",
|
||||
maintainer_email = "fpeters@entrouvert.com",
|
||||
url = "http://wcs.labs.libre-entreprise.org",
|
||||
install_requires=[
|
||||
'gadjo>=0.53',
|
||||
'django-ckeditor<=4.5.3',
|
||||
'django-ratelimit<3',
|
||||
'XStatic-Leaflet',
|
||||
'pyproj',
|
||||
],
|
||||
package_dir = { 'wcs': 'wcs' },
|
||||
packages = find_packages(),
|
||||
cmdclass = cmdclass,
|
||||
scripts = ['wcsctl.py', 'manage.py'],
|
||||
include_package_data=True,
|
||||
data_files = data_tree('share/wcs/web/', 'data/web/') + \
|
||||
data_tree('share/wcs/themes/', 'data/themes/') + \
|
||||
data_tree('share/wcs/vendor/', 'data/vendor/') + \
|
||||
data_tree('share/wcs/qommon/', 'wcs/qommon/static/') +
|
||||
[('share/wcs/', ['data/webbots'])]
|
||||
)
|
||||
|
||||
if local_cfg:
|
||||
open('wcs/wcs_cfg.py', 'w').write(local_cfg)
|
||||
file('wcs/wcs_cfg.py', 'w').write(local_cfg)
|
||||
|
|
|
@ -1,166 +0,0 @@
|
|||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.qommon.ident.password_accounts import PasswordAccount
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub(request):
|
||||
pub = create_temporary_pub()
|
||||
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
pub.set_app_dir(req)
|
||||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.cfg['language'] = {'language': 'en'}
|
||||
pub.write_cfg()
|
||||
|
||||
return pub
|
||||
|
||||
|
||||
def create_superuser(pub):
|
||||
if pub.user_class.select(lambda x: x.name == 'admin'):
|
||||
user1 = pub.user_class.select(lambda x: x.name == 'admin')[0]
|
||||
user1.is_admin = True
|
||||
user1.store()
|
||||
return user1
|
||||
|
||||
user1 = pub.user_class(name='admin')
|
||||
user1.is_admin = True
|
||||
user1.email = 'admin@example.com'
|
||||
user1.store()
|
||||
|
||||
account1 = PasswordAccount(id='admin')
|
||||
account1.set_password('admin')
|
||||
account1.user_id = user1.id
|
||||
account1.store()
|
||||
|
||||
return user1
|
||||
|
||||
|
||||
def create_role(pub):
|
||||
pub.role_class.wipe()
|
||||
role = pub.role_class(name='foobar')
|
||||
role.store()
|
||||
return role
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_empty_site(pub):
|
||||
pub.user_class.wipe()
|
||||
resp = get_app(pub).get('/backoffice/users/')
|
||||
resp = resp.click('New User')
|
||||
resp = get_app(pub).get('/backoffice/settings/')
|
||||
|
||||
|
||||
def test_empty_site_but_idp_settings(pub):
|
||||
pub.cfg['idp'] = {'xxx': {}}
|
||||
pub.write_cfg()
|
||||
resp = get_app(pub).get('/backoffice/')
|
||||
assert resp.location == 'http://example.net/login/?next=http%3A%2F%2Fexample.net%2Fbackoffice%2F'
|
||||
|
||||
|
||||
def test_with_user(pub):
|
||||
create_superuser(pub)
|
||||
resp = get_app(pub).get('/backoffice/', status=302)
|
||||
assert resp.location == 'http://example.net/login/?next=http%3A%2F%2Fexample.net%2Fbackoffice%2F'
|
||||
|
||||
|
||||
def test_with_superuser(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
app.get('/backoffice/')
|
||||
|
||||
|
||||
def test_admin_redirect(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
assert app.get('/admin/whatever', status=302).location == 'http://example.net/backoffice/whatever'
|
||||
|
||||
|
||||
def test_admin_for_all(pub):
|
||||
user = create_superuser(pub)
|
||||
role = create_role(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test'
|
||||
formdef.store()
|
||||
|
||||
try:
|
||||
with open(os.path.join(pub.app_dir, 'ADMIN_FOR_ALL'), 'w'):
|
||||
pass # create empty file
|
||||
resp = get_app(pub).get('/backoffice/')
|
||||
assert resp.location.endswith('studio/')
|
||||
resp = resp.follow()
|
||||
# check there is a CSS class
|
||||
assert resp.pyquery.find('body.admin-for-all')
|
||||
|
||||
# check there are menu items
|
||||
resp.click('Forms', index=0)
|
||||
resp.click('Settings', index=0)
|
||||
|
||||
# cheeck it's possible to get inside the subdirectories
|
||||
resp = get_app(pub).get('/backoffice/settings/', status=200)
|
||||
|
||||
pub.cfg['admin-permissions'] = {'settings': [role.id]}
|
||||
pub.write_cfg()
|
||||
resp = get_app(pub).get('/backoffice/settings/', status=200)
|
||||
|
||||
# check it doesn't work with a non-empty ADMIN_FOR_ALL file
|
||||
with open(os.path.join(pub.app_dir, 'ADMIN_FOR_ALL'), 'w') as fd:
|
||||
fd.write('x.x.x.x')
|
||||
resp = get_app(pub).get('/backoffice/settings/', status=302)
|
||||
|
||||
# check it works if the file contains the user IP address
|
||||
with open(os.path.join(pub.app_dir, 'ADMIN_FOR_ALL'), 'w') as fd:
|
||||
fd.write('127.0.0.1')
|
||||
resp = get_app(pub).get('/backoffice/settings/', status=200)
|
||||
|
||||
# check it's also ok if the user is logged in but doesn't have the
|
||||
# permissions
|
||||
user.is_admin = False
|
||||
user.store()
|
||||
resp = login(get_app(pub)).get('/backoffice/settings/', status=200)
|
||||
# check there are menu items
|
||||
resp.click('Management', index=0)
|
||||
resp.click('Forms', index=0)
|
||||
resp.click('Settings', index=0)
|
||||
|
||||
finally:
|
||||
if 'admin-permissions' in pub.cfg:
|
||||
del pub.cfg['admin-permissions']
|
||||
pub.write_cfg()
|
||||
os.unlink(os.path.join(pub.app_dir, 'ADMIN_FOR_ALL'))
|
||||
role.remove_self()
|
||||
user.is_admin = True
|
||||
user.store()
|
||||
|
||||
|
||||
def test_users_roles_menu_entries(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/management/forms')
|
||||
assert resp.pyquery('#sidepage-menu .icon-users')
|
||||
assert resp.pyquery('#sidepage-menu .icon-roles')
|
||||
resp = app.get('/backoffice/menu.json')
|
||||
assert 'Users' in [x['label'] for x in resp.json]
|
||||
assert 'Roles' in [x['label'] for x in resp.json]
|
||||
|
||||
# don't include users/roles in menu if roles are managed by an external
|
||||
# identity provider.
|
||||
pub.cfg['sp'] = {'idp-manage-roles': True}
|
||||
pub.write_cfg()
|
||||
|
||||
resp = app.get('/backoffice/management/forms')
|
||||
assert not resp.pyquery('#sidepage-menu .icon-users')
|
||||
assert not resp.pyquery('#sidepage-menu .icon-roles')
|
||||
resp = app.get('/backoffice/menu.json')
|
||||
assert 'Users' not in [x['label'] for x in resp.json]
|
||||
assert 'Roles' not in [x['label'] for x in resp.json]
|
|
@ -1,193 +0,0 @@
|
|||
# w.c.s. - web application for online forms
|
||||
# Copyright (C) 2005-2020 Entr'ouvert
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import pytest
|
||||
|
||||
from wcs.api_access import ApiAccess
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import create_superuser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub(request):
|
||||
pub = create_temporary_pub()
|
||||
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
pub.set_app_dir(req)
|
||||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.cfg['language'] = {'language': 'en'}
|
||||
pub.write_cfg()
|
||||
|
||||
return pub
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api_access():
|
||||
ApiAccess.wipe()
|
||||
obj = ApiAccess()
|
||||
obj.name = 'Jhon'
|
||||
obj.description = 'API key for Jhon'
|
||||
obj.access_identifier = 'jhon'
|
||||
obj.access_key = '12345'
|
||||
obj.store()
|
||||
return obj
|
||||
|
||||
|
||||
def test_api_access_new(pub):
|
||||
create_superuser(pub)
|
||||
ApiAccess.wipe()
|
||||
app = login(get_app(pub))
|
||||
|
||||
# go to the page and cancel
|
||||
resp = app.get('/backoffice/settings/api-access/')
|
||||
resp = resp.click('New API access')
|
||||
resp = resp.forms[0].submit('cancel')
|
||||
assert resp.location == 'http://example.net/backoffice/settings/api-access/'
|
||||
|
||||
# go to the page and add an API access
|
||||
resp = app.get('/backoffice/settings/api-access/')
|
||||
resp = resp.click('New API access')
|
||||
resp.form['name'] = 'a new API access'
|
||||
resp.form['description'] = 'description'
|
||||
resp.form['access_identifier'] = 'new_access'
|
||||
assert len(resp.form['access_key'].value) == 36
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/settings/api-access/'
|
||||
resp = resp.follow()
|
||||
assert 'a new API access' in resp.text
|
||||
resp = resp.click('a new API access')
|
||||
assert 'API access - a new API access' in resp.text
|
||||
|
||||
# check name unicity
|
||||
resp = app.get('/backoffice/settings/api-access/new')
|
||||
resp.form['name'] = 'a new API access'
|
||||
resp.form['access_identifier'] = 'changed'
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.html.find('div', {'class': 'error'}).text == 'This name is already used.'
|
||||
|
||||
# check access_identifier unicity
|
||||
resp.form['name'] = 'new one'
|
||||
resp.form['access_identifier'] = 'new_access'
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.html.find('div', {'class': 'error'}).text == 'This value is already used.'
|
||||
|
||||
|
||||
def test_api_access_view(pub, api_access):
|
||||
create_superuser(pub)
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/settings/api-access/%s/' % api_access.id)
|
||||
assert '12345' in resp.text
|
||||
|
||||
resp = app.get('/backoffice/settings/api-access/wrong-id/', status=404)
|
||||
|
||||
|
||||
def test_api_access_edit(pub, api_access):
|
||||
create_superuser(pub)
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/settings/api-access/1/')
|
||||
resp = resp.click(href='edit')
|
||||
assert resp.form['name'].value == 'Jhon'
|
||||
resp = resp.form.submit('cancel')
|
||||
assert resp.location == 'http://example.net/backoffice/settings/api-access/1/'
|
||||
resp = resp.follow()
|
||||
resp = resp.click(href='edit')
|
||||
resp.form['name'] = 'Smith Robert'
|
||||
resp.form['description'] = 'bla bla bla'
|
||||
resp.form['access_identifier'] = 'smith2'
|
||||
resp.form['access_key'] = '5678'
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/settings/api-access/1/'
|
||||
resp = resp.follow()
|
||||
|
||||
api_access = ApiAccess.get('1')
|
||||
assert api_access.name == 'Smith Robert'
|
||||
assert api_access.description == 'bla bla bla'
|
||||
assert api_access.access_identifier == 'smith2'
|
||||
assert api_access.access_key == '5678'
|
||||
|
||||
# check name unicity
|
||||
resp = app.get('/backoffice/settings/api-access/new')
|
||||
resp.form['name'] = 'Jhon'
|
||||
resp.form['access_identifier'] = 'jhon'
|
||||
resp = resp.form.submit('submit')
|
||||
resp = app.get('/backoffice/settings/api-access/1/')
|
||||
resp = resp.click(href='edit')
|
||||
resp.form['name'] = 'Jhon'
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.html.find('div', {'class': 'error'}).text == 'This name is already used.'
|
||||
|
||||
|
||||
def test_api_access_delete(pub, api_access):
|
||||
create_superuser(pub)
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/settings/api-access/1/')
|
||||
resp = resp.click(href='delete')
|
||||
resp = resp.form.submit('cancel')
|
||||
assert resp.location == 'http://example.net/backoffice/settings/api-access/'
|
||||
|
||||
resp = app.get('/backoffice/settings/api-access/1/')
|
||||
resp = resp.click(href='delete')
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/settings/api-access/'
|
||||
assert ApiAccess.count() == 0
|
||||
|
||||
|
||||
def test_api_access_roles(pub, api_access):
|
||||
create_superuser(pub)
|
||||
|
||||
pub.role_class.wipe()
|
||||
role_a = pub.role_class(name='a')
|
||||
role_a.store()
|
||||
role_b = pub.role_class(name='b')
|
||||
role_b.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/settings/api-access/1/')
|
||||
resp = resp.click(href='edit')
|
||||
resp.form['roles$element0'] = role_a.id
|
||||
resp = resp.form.submit('roles$add_element')
|
||||
resp.form['roles$element1'] = role_b.id
|
||||
resp = resp.form.submit('submit')
|
||||
|
||||
api_access = ApiAccess.get(api_access.id)
|
||||
assert {x.id for x in api_access.get_roles()} == {role_a.id, role_b.id}
|
||||
|
||||
|
||||
def test_api_access_disabled(pub):
|
||||
create_superuser(pub)
|
||||
ApiAccess.wipe()
|
||||
|
||||
pub.cfg['idp'] = {'xxx': {'metadata_url': 'http://idp.example.net/idp/saml2/metadata'}}
|
||||
pub.write_cfg()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/settings/api-access/')
|
||||
assert 'New API access' not in resp.text
|
||||
|
||||
assert 'API accesses are now globally managed on the identity provider.' in resp.text
|
||||
assert resp.pyquery('.infonotice a.pk-button').attr.href == 'http://idp.example.net/manage/api-clients/'
|
|
@ -1,744 +0,0 @@
|
|||
import os
|
||||
import re
|
||||
|
||||
import pytest
|
||||
from webtest import Upload
|
||||
|
||||
from wcs import fields
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.categories import BlockCategory
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.testdef import TestDef, TestResult
|
||||
from wcs.workflows import Workflow
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import create_role, create_superuser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub(request):
|
||||
pub = create_temporary_pub()
|
||||
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
pub.set_app_dir(req)
|
||||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.cfg['language'] = {'language': 'en'}
|
||||
pub.write_cfg()
|
||||
|
||||
return pub
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_block_404(pub):
|
||||
create_superuser(pub)
|
||||
create_role(pub)
|
||||
BlockDef.wipe()
|
||||
app = login(get_app(pub))
|
||||
app.get('/backoffice/forms/blocks/1/', status=404)
|
||||
|
||||
|
||||
def test_block_new(pub):
|
||||
create_superuser(pub)
|
||||
create_role(pub)
|
||||
BlockDef.wipe()
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/')
|
||||
resp = resp.click('Fields blocks')
|
||||
resp = resp.click('New field block')
|
||||
resp.form['name'] = 'field block'
|
||||
resp = resp.form.submit()
|
||||
assert resp.location == 'http://example.net/backoffice/forms/blocks/1/'
|
||||
resp = resp.follow()
|
||||
assert '<h2>field block' in resp
|
||||
assert 'There are not yet any fields' in resp
|
||||
|
||||
resp.form['label'] = 'foobar'
|
||||
resp.form['type'] = 'string'
|
||||
resp = resp.form.submit()
|
||||
assert resp.location == 'http://example.net/backoffice/forms/blocks/1/'
|
||||
resp = resp.follow()
|
||||
|
||||
resp.form['label'] = 'barfoo'
|
||||
resp.form['type'] = 'string'
|
||||
resp = resp.form.submit()
|
||||
assert resp.location == 'http://example.net/backoffice/forms/blocks/1/'
|
||||
resp = resp.follow()
|
||||
|
||||
assert len(BlockDef.get(1).fields) == 2
|
||||
assert str(BlockDef.get(1).fields[0].id) != '1' # don't use integers
|
||||
|
||||
|
||||
def test_block_options(pub):
|
||||
create_superuser(pub)
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test')]
|
||||
block.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^settings$'))
|
||||
assert 'readonly' not in resp.form['slug'].attrs
|
||||
resp.form['name'] = 'foo bar'
|
||||
resp = resp.form.submit('submit')
|
||||
assert BlockDef.get(block.id).name == 'foo bar'
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='0', label='test', block_slug=block.slug),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^settings$'))
|
||||
assert 'readonly' in resp.form['slug'].attrs
|
||||
resp = resp.form.submit('cancel')
|
||||
resp = resp.follow()
|
||||
|
||||
|
||||
def test_block_options_slug(pub):
|
||||
create_superuser(pub)
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foo'
|
||||
block.fields = []
|
||||
block.store()
|
||||
|
||||
block2 = BlockDef()
|
||||
block2.name = 'bar'
|
||||
block2.fields = []
|
||||
block2.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/settings' % block.id)
|
||||
resp.form['slug'] = 'bar'
|
||||
resp = resp.form.submit('submit')
|
||||
assert 'This identifier is already used.' in resp.text
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/%s/settings' % block.id)
|
||||
resp.form['slug'] = 'foo'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/%s/settings' % block.id)
|
||||
resp.form['slug'] = 'foo2'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
block.refresh_from_storage()
|
||||
assert block.slug == 'foo2'
|
||||
|
||||
|
||||
def test_block_options_digest_template(pub):
|
||||
create_superuser(pub)
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = []
|
||||
block.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/settings' % block.id)
|
||||
resp.form['digest_template'] = 'X{{form_var_foo}}Y'
|
||||
resp = resp.form.submit('submit')
|
||||
assert (
|
||||
'Wrong variable "form_var_…" detected. Please replace it by "block_var_…".'
|
||||
in resp.text
|
||||
)
|
||||
block = BlockDef.get(block.id)
|
||||
assert block.digest_template is None
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/%s/settings' % block.id)
|
||||
resp.form['digest_template'] = 'X{{block_var_foo}}Y'
|
||||
resp = resp.form.submit('submit')
|
||||
block = BlockDef.get(block.id)
|
||||
assert block.digest_template == 'X{{block_var_foo}}Y'
|
||||
|
||||
|
||||
def test_block_export_import(pub):
|
||||
create_superuser(pub)
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test')]
|
||||
block.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^export$'))
|
||||
xml_export = resp.text
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/')
|
||||
resp = resp.click(href='import')
|
||||
resp = resp.form.submit('cancel') # shouldn't block on missing file
|
||||
resp = resp.follow()
|
||||
|
||||
resp = resp.click(href='import')
|
||||
resp = resp.form.submit()
|
||||
assert 'ere were errors processing your form.' in resp
|
||||
|
||||
resp.form['file'] = Upload('block', xml_export.encode('utf-8'))
|
||||
resp = resp.form.submit()
|
||||
resp = resp.follow()
|
||||
assert BlockDef.count() == 2
|
||||
|
||||
new_blockdef = [x for x in BlockDef.select() if str(x.id) != str(block.id)][0]
|
||||
assert new_blockdef.name == 'Copy of foobar'
|
||||
assert new_blockdef.slug == 'foobar_1'
|
||||
assert len(new_blockdef.fields) == 1
|
||||
assert new_blockdef.fields[0].id == '123'
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/')
|
||||
resp = resp.click(href='import')
|
||||
resp.form['file'] = Upload('block', xml_export.encode('utf-8'))
|
||||
resp = resp.form.submit()
|
||||
assert 'Copy of foobar (2)' in [x.name for x in BlockDef.select()]
|
||||
|
||||
# import invalid content
|
||||
resp = app.get('/backoffice/forms/blocks/')
|
||||
resp = resp.click(href='import')
|
||||
resp.form['file'] = Upload('block', b'whatever')
|
||||
resp = resp.form.submit()
|
||||
assert 'Invalid File' in resp
|
||||
|
||||
# unknown reference
|
||||
block.fields = [
|
||||
fields.StringField(id='1', data_source={'type': 'foobar'}),
|
||||
]
|
||||
block.store()
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^export$'))
|
||||
xml_export = resp.text
|
||||
resp = app.get('/backoffice/forms/blocks/')
|
||||
resp = resp.click(href='import')
|
||||
resp.form['file'] = Upload('block', xml_export.encode('utf-8'))
|
||||
resp = resp.form.submit()
|
||||
assert 'Invalid File (Unknown referenced objects)' in resp
|
||||
assert '<ul><li>Unknown datasources: foobar</li></ul>' in resp
|
||||
|
||||
# python expression
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'forbid-new-python-expressions', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
block.fields = [
|
||||
fields.StringField(id='2', label='python_prefill', prefill={'type': 'formula', 'value': '1 + 2'}),
|
||||
]
|
||||
block.store()
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^export$'))
|
||||
xml_export = resp.text
|
||||
resp = app.get('/backoffice/forms/blocks/')
|
||||
resp = resp.click(href='import')
|
||||
resp.form['file'] = Upload('block', xml_export.encode('utf-8'))
|
||||
resp = resp.form.submit()
|
||||
assert 'Python expression detected' in resp
|
||||
|
||||
|
||||
def test_block_delete(pub):
|
||||
create_superuser(pub)
|
||||
BlockDef.wipe()
|
||||
FormDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test')]
|
||||
block.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^delete$'))
|
||||
assert 'You are about to irrevocably delete this block.' in resp
|
||||
resp = resp.form.submit()
|
||||
resp = resp.follow()
|
||||
assert BlockDef.count() == 0
|
||||
|
||||
# in use
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test')]
|
||||
block.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='0', label='test', block_slug=block.slug),
|
||||
]
|
||||
formdef.store()
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^delete$'))
|
||||
assert 'This block is still used' in resp
|
||||
|
||||
|
||||
def test_block_export_overwrite(pub):
|
||||
create_superuser(pub)
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test')]
|
||||
block.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^export$'))
|
||||
xml_export = resp.text
|
||||
|
||||
block.slug = 'new-slug'
|
||||
block.name = 'New foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test bebore overwrite')]
|
||||
block.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click('Overwrite')
|
||||
resp = resp.form.submit('cancel').follow()
|
||||
resp = resp.click('Overwrite')
|
||||
resp = resp.form.submit()
|
||||
assert 'There were errors processing your form.' in resp
|
||||
|
||||
resp.form['file'] = Upload('block', xml_export.encode('utf-8'))
|
||||
resp = resp.form.submit()
|
||||
resp = resp.follow()
|
||||
assert BlockDef.count() == 1
|
||||
|
||||
block.refresh_from_storage()
|
||||
assert block.fields[0].label == 'Test'
|
||||
assert block.name == 'foobar'
|
||||
assert block.slug == 'new-slug' # not overwritten
|
||||
|
||||
# unknown reference
|
||||
block.fields = [fields.StringField(id='1', data_source={'type': 'foobar'})]
|
||||
block.store()
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^export$'))
|
||||
xml_export = resp.text
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click('Overwrite')
|
||||
resp.form['file'] = Upload('block', xml_export.encode('utf-8'))
|
||||
resp = resp.form.submit()
|
||||
assert 'Invalid File (Unknown referenced objects)' in resp
|
||||
assert '<ul><li>Unknown datasources: foobar</li></ul>' in resp
|
||||
|
||||
|
||||
def test_block_edit_duplicate_delete_field(pub):
|
||||
create_superuser(pub)
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test')]
|
||||
block.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('123/$'))
|
||||
resp.form['required'].checked = False
|
||||
resp.form['varname'] = 'test'
|
||||
resp = resp.form.submit('submit')
|
||||
resp = resp.follow()
|
||||
assert BlockDef.get(block.id).fields[0].required is False
|
||||
assert BlockDef.get(block.id).fields[0].varname == 'test'
|
||||
|
||||
resp = resp.click(href=re.compile('123/duplicate$'))
|
||||
resp = resp.follow()
|
||||
assert len(BlockDef.get(block.id).fields) == 2
|
||||
|
||||
resp = resp.click(href='%s/delete' % BlockDef.get(block.id).fields[1].id)
|
||||
resp = resp.form.submit('submit')
|
||||
resp = resp.follow()
|
||||
assert len(BlockDef.get(block.id).fields) == 1
|
||||
|
||||
|
||||
def test_block_use_in_formdef(pub):
|
||||
create_superuser(pub)
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test')]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = []
|
||||
formdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/1/fields/')
|
||||
resp.forms[0]['label'] = 'a block field'
|
||||
resp.forms[0]['type'] = 'block:foobar'
|
||||
resp = resp.forms[0].submit().follow()
|
||||
formdef.refresh_from_storage()
|
||||
assert 'a block field' in resp.text
|
||||
resp = resp.click('Edit', href='%s/' % formdef.fields[0].id)
|
||||
assert resp.pyquery('.field-edit--title').text() == 'a block field'
|
||||
assert resp.pyquery('.field-edit--subtitle').text() == 'Block of fields - foobar'
|
||||
assert resp.pyquery('.field-edit--subtitle a').attr.href.endswith(
|
||||
'/backoffice/forms/blocks/%s/' % block.id
|
||||
)
|
||||
assert resp.form['max_items'].value == '1'
|
||||
|
||||
# check it's not possible to have an empty max_items
|
||||
resp.form['max_items'] = ''
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.pyquery('#form_error_max_items').text() == 'required field'
|
||||
|
||||
# check there's no crash if block is missing
|
||||
block.remove_self()
|
||||
resp = app.get(formdef.get_admin_url() + 'fields/')
|
||||
assert resp.pyquery('#fields-list .type-block .type').text() == 'Block of fields (foobar, missing)'
|
||||
resp = resp.click('Edit', href='%s/' % formdef.fields[0].id)
|
||||
assert resp.pyquery('.field-edit--subtitle').text() == 'Block of fields (foobar, missing)'
|
||||
|
||||
|
||||
def test_block_use_in_workflow_backoffice_fields(pub):
|
||||
create_superuser(pub)
|
||||
FormDef.wipe()
|
||||
Workflow.wipe()
|
||||
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test')]
|
||||
block.store()
|
||||
|
||||
workflow = Workflow(name='test')
|
||||
workflow.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get(workflow.get_admin_url())
|
||||
resp = resp.click(href='backoffice-fields/').follow()
|
||||
resp.forms[0]['label'] = 'a block field'
|
||||
resp.forms[0]['type'] = 'block:foobar'
|
||||
resp = resp.forms[0].submit().follow()
|
||||
resp = resp.click('Edit')
|
||||
assert resp.form['max_items'].value == '1'
|
||||
|
||||
|
||||
def test_blocks_category(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
BlockCategory.wipe()
|
||||
BlockDef.wipe()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/new')
|
||||
assert 'category_id' not in resp.form.fields
|
||||
|
||||
block = BlockDef(name='foo')
|
||||
block.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/categories/')
|
||||
resp = resp.click('New Category')
|
||||
resp.forms[0]['name'] = 'a new category'
|
||||
resp.forms[0]['description'] = 'description of the category'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert BlockCategory.count() == 1
|
||||
category = BlockCategory.select()[0]
|
||||
assert category.name == 'a new category'
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/new')
|
||||
assert 'category_id' in resp.form.fields
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^settings$'))
|
||||
resp.forms[0]['category_id'] = str(category.id)
|
||||
resp = resp.forms[0].submit('cancel').follow()
|
||||
block.refresh_from_storage()
|
||||
assert block.category_id is None
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^settings$'))
|
||||
resp.forms[0]['category_id'] = str(category.id)
|
||||
resp = resp.forms[0].submit('submit').follow()
|
||||
block.refresh_from_storage()
|
||||
assert str(block.category_id) == str(category.id)
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^settings$'))
|
||||
assert resp.forms[0]['category_id'].value == str(category.id)
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/categories/')
|
||||
resp = resp.click('New Category')
|
||||
resp.forms[0]['name'] = 'a second category'
|
||||
resp.forms[0]['description'] = 'description of the category'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert BlockCategory.count() == 2
|
||||
category2 = [x for x in BlockCategory.select() if x.id != category.id][0]
|
||||
assert category2.name == 'a second category'
|
||||
|
||||
app.get('/backoffice/forms/blocks/categories/update_order?order=%s;%s;' % (category2.id, category.id))
|
||||
categories = BlockCategory.select()
|
||||
BlockCategory.sort_by_position(categories)
|
||||
assert [x.id for x in categories] == [str(category2.id), str(category.id)]
|
||||
|
||||
app.get('/backoffice/forms/blocks/categories/update_order?order=%s;%s;0' % (category.id, category2.id))
|
||||
categories = BlockCategory.select()
|
||||
BlockCategory.sort_by_position(categories)
|
||||
assert [x.id for x in categories] == [str(category.id), str(category2.id)]
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/categories/')
|
||||
resp = resp.click('a new category')
|
||||
resp = resp.click('Delete')
|
||||
resp = resp.forms[0].submit()
|
||||
block.refresh_from_storage()
|
||||
assert not block.category_id
|
||||
|
||||
|
||||
def test_removed_block_in_form_fields_list(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='0', label='test', block_slug='removed'),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/%s/fields/' % formdef.id)
|
||||
assert 'Block of fields (removed, missing)' in resp.text
|
||||
|
||||
|
||||
def test_block_edit_field_warnings(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'ignore-hard-limits', 'false')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
BlockDef.wipe()
|
||||
blockdef = BlockDef()
|
||||
blockdef.name = 'block title'
|
||||
blockdef.fields = [fields.StringField(id='%d' % i, label='field %d' % i) for i in range(1, 10)]
|
||||
blockdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % blockdef.id)
|
||||
assert 'more than 20 fields' not in resp.text
|
||||
|
||||
blockdef.fields.extend([fields.StringField(id='%d' % i, label='field %d' % i) for i in range(10, 31)])
|
||||
blockdef.store()
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % blockdef.id)
|
||||
assert 'more than 30 fields' not in resp.text
|
||||
assert resp.pyquery('#new-field')
|
||||
assert resp.pyquery('#fields-list a[title="Duplicate"]').length
|
||||
|
||||
blockdef.fields.extend([fields.StringField(id='%d' % i, label='field %d' % i) for i in range(21, 51)])
|
||||
blockdef.store()
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % blockdef.id)
|
||||
assert 'This block of fields contains 60 fields.' in resp.text
|
||||
assert not resp.pyquery('#new-field')
|
||||
assert not resp.pyquery('#fields-list a[title="Duplicate"]').length
|
||||
|
||||
|
||||
def test_block_inspect(pub):
|
||||
create_superuser(pub)
|
||||
Workflow.wipe()
|
||||
BlockDef.wipe()
|
||||
FormDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.StringField(id='123', required=True, label='Test'),
|
||||
fields.StringField(id='124', required=True, label='Test2'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [fields.BlockField(id='0', label='first test', block_slug=block.slug)]
|
||||
formdef.store()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title 2'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='0', label='second test', block_slug=block.slug, max_items=3, remove_button=True)
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click('Inspector')
|
||||
assert resp.pyquery('#inspect-fields .inspect-field').length == 2
|
||||
assert '2 fields.' in resp.text
|
||||
assert resp.pyquery('table.block-usage tbody tr').length == 2
|
||||
assert 'second test 3 yes' in resp.pyquery('table.block-usage tbody tr td').text()
|
||||
assert 'first test 1 no' in resp.pyquery('table.block-usage tbody tr td').text()
|
||||
|
||||
|
||||
def test_block_duplicate(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'Foobar'
|
||||
block.fields = [
|
||||
fields.StringField(id='123', required=True, label='Test'),
|
||||
fields.StringField(id='124', required=True, label='Test2'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
|
||||
resp = resp.click(href=re.compile('^duplicate$'))
|
||||
assert resp.form['name'].value == 'Foobar (copy)'
|
||||
resp = resp.form.submit('cancel').follow()
|
||||
assert BlockDef.count() == 1
|
||||
|
||||
resp = resp.click(href=re.compile('^duplicate$'))
|
||||
assert resp.form['name'].value == 'Foobar (copy)'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert BlockDef.count() == 2
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^duplicate$'))
|
||||
assert resp.form['name'].value == 'Foobar (copy 2)'
|
||||
resp.form['name'].value = 'other copy'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert BlockDef.count() == 3
|
||||
assert {x.name for x in BlockDef.select()} == {'Foobar', 'Foobar (copy)', 'other copy'}
|
||||
assert {x.slug for x in BlockDef.select()} == {'foobar', 'foobar_copy', 'other_copy'}
|
||||
|
||||
block_copy = BlockDef.get_by_slug('other_copy')
|
||||
assert len(block_copy.fields) == 2
|
||||
|
||||
|
||||
def test_block_field_statistics_data_update(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'Foobar'
|
||||
block.fields = [fields.BoolField(id='1', label='Bool', varname='bool')]
|
||||
block.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='0', label='test', block_slug=block.slug),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.data['0'] = {'data': [{'1': True}]}
|
||||
formdata.store()
|
||||
|
||||
assert not formdata.statistics_data
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/1/' % block.id)
|
||||
|
||||
resp.form['display_locations$element3'] = True
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert 'Statistics data will be collected in the background.' in resp.text
|
||||
|
||||
formdata.refresh_from_storage()
|
||||
assert formdata.statistics_data == {'bool': [True]}
|
||||
|
||||
|
||||
def test_block_test_results(pub):
|
||||
create_superuser(pub)
|
||||
TestDef.wipe()
|
||||
TestResult.wipe()
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test')]
|
||||
block.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='0', label='test', block_slug=block.slug),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('123/$'))
|
||||
resp.form['varname'] = 'test'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert TestResult.count() == 0
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.data['1'] = 'a'
|
||||
formdata.store()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.name = 'First test'
|
||||
testdef.store()
|
||||
|
||||
resp = resp.click(href=re.compile('123/$'))
|
||||
resp.form['varname'] = 'test_3'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
assert TestResult.count() == 1
|
||||
|
||||
|
||||
def test_block_documentation(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
BlockDef.wipe()
|
||||
blockdef = FormDef()
|
||||
blockdef.name = 'block title'
|
||||
blockdef.fields = [fields.BoolField(id='1', label='Bool')]
|
||||
blockdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get(blockdef.get_admin_url())
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
resp = app.post_json(blockdef.get_admin_url() + 'update-documentation', {'content': '<p>doc</p>'})
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
blockdef.refresh_from_storage()
|
||||
assert blockdef.documentation == '<p>doc</p>'
|
||||
resp = app.get(blockdef.get_admin_url())
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
|
||||
resp = app.get(blockdef.get_admin_url() + 'fields/1/')
|
||||
assert resp.pyquery('.documentation[hidden]')
|
||||
assert resp.pyquery('#sidebar[hidden]')
|
||||
resp = app.post_json(
|
||||
blockdef.get_admin_url() + 'fields/1/update-documentation', {'content': '<p>doc</p>'}
|
||||
)
|
||||
assert resp.json == {'err': 0, 'empty': False, 'changed': True}
|
||||
blockdef.refresh_from_storage()
|
||||
assert blockdef.fields[0].documentation == '<p>doc</p>'
|
||||
resp = app.get(blockdef.get_admin_url() + 'fields/1/')
|
||||
assert resp.pyquery('.documentation:not([hidden])')
|
||||
assert resp.pyquery('#sidebar:not([hidden])')
|
||||
|
||||
|
||||
def test_block_options_post_conditions(pub):
|
||||
create_superuser(pub)
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test')]
|
||||
block.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get(f'/backoffice/forms/blocks/{block.id}/settings')
|
||||
resp.form['post_conditions$element0$condition$value_django'] = 'condition_1'
|
||||
resp.form['post_conditions$element0$error_message'] = 'error 1'
|
||||
resp = resp.form.submit('post_conditions$add_element')
|
||||
resp.form['post_conditions$element1$condition$value_django'] = 'condition_2'
|
||||
resp.form['post_conditions$element1$error_message'] = 'error 2'
|
||||
resp = resp.form.submit('submit')
|
||||
block.refresh_from_storage()
|
||||
assert block.post_conditions == [
|
||||
{'condition': {'type': 'django', 'value': 'condition_1'}, 'error_message': 'error 1'},
|
||||
{'condition': {'type': 'django', 'value': 'condition_2'}, 'error_message': 'error 2'},
|
||||
]
|
|
@ -1,229 +0,0 @@
|
|||
import pytest
|
||||
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.categories import CardDefCategory, Category
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import create_superuser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub(request):
|
||||
pub = create_temporary_pub()
|
||||
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
pub.set_app_dir(req)
|
||||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.cfg['language'] = {'language': 'en'}
|
||||
pub.write_cfg()
|
||||
|
||||
return pub
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_categories(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
app.get('/backoffice/cards/categories/')
|
||||
|
||||
|
||||
def test_categories_new(pub):
|
||||
create_superuser(pub)
|
||||
CardDefCategory.wipe()
|
||||
app = login(get_app(pub))
|
||||
|
||||
# go to the page and cancel
|
||||
resp = app.get('/backoffice/cards/categories/')
|
||||
resp = resp.click('New Category')
|
||||
resp = resp.forms[0].submit('cancel')
|
||||
assert resp.location == 'http://example.net/backoffice/cards/categories/'
|
||||
|
||||
# go to the page and add a category
|
||||
resp = app.get('/backoffice/cards/categories/')
|
||||
resp = resp.click('New Category')
|
||||
resp.forms[0]['name'] = 'a new category'
|
||||
resp.forms[0]['description'] = 'description of the category'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/cards/categories/'
|
||||
resp = resp.follow()
|
||||
assert 'a new category' in resp.text
|
||||
resp = resp.click('a new category')
|
||||
assert '<h2>a new category' in resp.text
|
||||
|
||||
assert CardDefCategory.get(1).name == 'a new category'
|
||||
assert CardDefCategory.get(1).description == 'description of the category'
|
||||
|
||||
|
||||
def test_categories_edit(pub):
|
||||
create_superuser(pub)
|
||||
CardDefCategory.wipe()
|
||||
category = CardDefCategory(name='foobar')
|
||||
category.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/cards/categories/1/')
|
||||
assert 'No card model associated to this category' in resp.text
|
||||
|
||||
resp = resp.click(href='edit')
|
||||
assert resp.forms[0]['name'].value == 'foobar'
|
||||
resp.forms[0]['description'] = 'category description'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/cards/categories/'
|
||||
resp = resp.follow()
|
||||
resp = resp.click('foobar')
|
||||
assert '<h2>foobar' in resp.text
|
||||
|
||||
assert CardDefCategory.get(1).description == 'category description'
|
||||
|
||||
|
||||
def test_categories_edit_duplicate_name(pub):
|
||||
CardDefCategory.wipe()
|
||||
category = CardDefCategory(name='foobar')
|
||||
category.store()
|
||||
category = CardDefCategory(name='foobar2')
|
||||
category.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/cards/categories/1/')
|
||||
|
||||
resp = resp.click(href='edit')
|
||||
assert resp.forms[0]['name'].value == 'foobar'
|
||||
resp.forms[0]['name'] = 'foobar2'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert 'This name is already used' in resp.text
|
||||
|
||||
resp = resp.forms[0].submit('cancel')
|
||||
assert resp.location == 'http://example.net/backoffice/cards/categories/'
|
||||
|
||||
|
||||
def test_categories_with_carddefs(pub):
|
||||
CardDefCategory.wipe()
|
||||
category = CardDefCategory(name='foobar')
|
||||
category.store()
|
||||
|
||||
CardDef.wipe()
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/cards/categories/1/')
|
||||
assert 'form bar' not in resp.text
|
||||
|
||||
formdef = CardDef()
|
||||
formdef.name = 'form bar'
|
||||
formdef.fields = []
|
||||
formdef.category_id = category.id
|
||||
formdef.store()
|
||||
|
||||
resp = app.get('/backoffice/cards/categories/1/')
|
||||
assert 'form bar' in resp.text
|
||||
assert 'No card model associated to this category' not in resp.text
|
||||
|
||||
|
||||
def test_categories_delete(pub):
|
||||
create_superuser(pub)
|
||||
CardDefCategory.wipe()
|
||||
category = CardDefCategory(name='foobar')
|
||||
category.store()
|
||||
|
||||
CardDef.wipe()
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/cards/categories/1/')
|
||||
|
||||
resp = resp.click(href='delete')
|
||||
resp = resp.forms[0].submit('cancel')
|
||||
assert resp.location == 'http://example.net/backoffice/cards/categories/1/'
|
||||
assert CardDefCategory.count() == 1
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'bar'
|
||||
carddef.fields = []
|
||||
carddef.category_id = category.id
|
||||
carddef.store()
|
||||
|
||||
Category.wipe()
|
||||
formdef_category = Category(name='blah')
|
||||
formdef_category.id = category.id
|
||||
formdef_category.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'bar'
|
||||
formdef.fields = []
|
||||
formdef.category_id = formdef_category.id
|
||||
formdef.store()
|
||||
|
||||
resp = app.get('/backoffice/cards/categories/1/')
|
||||
resp = resp.click(href='delete')
|
||||
resp = resp.forms[0].submit()
|
||||
assert resp.location == 'http://example.net/backoffice/cards/categories/'
|
||||
resp = resp.follow()
|
||||
assert CardDefCategory.count() == 0
|
||||
|
||||
carddef.refresh_from_storage()
|
||||
assert carddef.category_id is None
|
||||
|
||||
formdef.refresh_from_storage()
|
||||
assert formdef.category_id == formdef_category.id
|
||||
|
||||
|
||||
def test_categories_edit_description(pub):
|
||||
CardDefCategory.wipe()
|
||||
category = CardDefCategory(name='foobar')
|
||||
category.description = 'category description'
|
||||
category.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
# this URL is used for editing from the frontoffice, there's no link
|
||||
# pointing to it in the admin.
|
||||
resp = app.get('/backoffice/cards/categories/1/description')
|
||||
assert resp.forms[0]['description'].value == 'category description'
|
||||
resp.forms[0]['description'] = 'updated description'
|
||||
|
||||
# check cancel doesn't save the change
|
||||
resp2 = resp.forms[0].submit('cancel')
|
||||
assert resp2.location == 'http://example.net/backoffice/cards/categories/1/'
|
||||
assert CardDefCategory.get(1).description == 'category description'
|
||||
|
||||
# check submit does it properly
|
||||
resp2 = resp.forms[0].submit('submit')
|
||||
assert resp2.location == 'http://example.net/backoffice/cards/categories/1/'
|
||||
resp2 = resp2.follow()
|
||||
assert CardDefCategory.get(1).description == 'updated description'
|
||||
|
||||
|
||||
def test_categories_new_duplicate_name(pub):
|
||||
CardDefCategory.wipe()
|
||||
category = CardDefCategory(name='foobar')
|
||||
category.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/cards/categories/')
|
||||
resp = resp.click('New Category')
|
||||
resp.forms[0]['name'] = 'foobar'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert 'This name is already used' in resp.text
|
||||
|
||||
|
||||
def test_categories_reorder(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
CardDefCategory.wipe()
|
||||
category = CardDefCategory(name='foo')
|
||||
category.store()
|
||||
category = CardDefCategory(name='bar')
|
||||
category.store()
|
||||
category = CardDefCategory(name='baz')
|
||||
category.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
app.get('/backoffice/cards/categories/update_order?order=1;2;3;')
|
||||
categories = CardDefCategory.select()
|
||||
CardDefCategory.sort_by_position(categories)
|
||||
assert [x.id for x in categories] == ['1', '2', '3']
|
||||
|
||||
app.get('/backoffice/cards/categories/update_order?order=3;1;2;0')
|
||||
categories = CardDefCategory.select()
|
||||
CardDefCategory.sort_by_position(categories)
|
||||
assert [x.id for x in categories] == ['3', '1', '2']
|
|
@ -1,305 +0,0 @@
|
|||
import io
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import pytest
|
||||
from webtest import Upload
|
||||
|
||||
from wcs.categories import CardDefCategory, Category
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import create_superuser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub(request):
|
||||
pub = create_temporary_pub()
|
||||
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
pub.set_app_dir(req)
|
||||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.cfg['language'] = {'language': 'en'}
|
||||
pub.write_cfg()
|
||||
|
||||
create_superuser(pub)
|
||||
|
||||
return pub
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_categories(pub):
|
||||
app = login(get_app(pub))
|
||||
app.get('/backoffice/forms/categories/')
|
||||
|
||||
|
||||
def test_categories_legacy_urls(pub):
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/categories/')
|
||||
assert resp.location.endswith('/backoffice/forms/categories/')
|
||||
resp = app.get('/backoffice/categories/1')
|
||||
assert resp.location.endswith('/backoffice/forms/categories/1')
|
||||
resp = app.get('/backoffice/categories/1/')
|
||||
assert resp.location.endswith('/backoffice/forms/categories/1/')
|
||||
|
||||
|
||||
def test_categories_new(pub):
|
||||
Category.wipe()
|
||||
app = login(get_app(pub))
|
||||
|
||||
# go to the page and cancel
|
||||
resp = app.get('/backoffice/forms/categories/')
|
||||
resp = resp.click('New Category')
|
||||
resp = resp.forms[0].submit('cancel')
|
||||
assert resp.location == 'http://example.net/backoffice/forms/categories/'
|
||||
|
||||
# go to the page and add a category
|
||||
resp = app.get('/backoffice/forms/categories/')
|
||||
resp = resp.click('New Category')
|
||||
resp.forms[0]['name'] = 'a new category'
|
||||
resp.forms[0]['description'] = 'description of the category'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/forms/categories/'
|
||||
resp = resp.follow()
|
||||
assert 'a new category' in resp.text
|
||||
resp = resp.click('a new category')
|
||||
assert '<h2>a new category' in resp.text
|
||||
|
||||
assert Category.get(1).name == 'a new category'
|
||||
assert Category.get(1).description == 'description of the category'
|
||||
|
||||
|
||||
def test_categories_edit(pub):
|
||||
Category.wipe()
|
||||
category = Category(name='foobar')
|
||||
category.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/categories/1/')
|
||||
assert 'No form associated to this category' in resp.text
|
||||
|
||||
resp = resp.click(href='edit')
|
||||
assert resp.forms[0]['name'].value == 'foobar'
|
||||
resp.forms[0]['description'] = 'category description'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/forms/categories/'
|
||||
resp = resp.follow()
|
||||
resp = resp.click('foobar')
|
||||
assert '<h2>foobar' in resp.text
|
||||
|
||||
assert Category.get(1).description == 'category description'
|
||||
|
||||
app.get('/backoffice/forms/categories/foo-bar/', status=404)
|
||||
|
||||
|
||||
def test_categories_edit_duplicate_name(pub):
|
||||
Category.wipe()
|
||||
category = Category(name='foobar')
|
||||
category.store()
|
||||
category = Category(name='foobar2')
|
||||
category.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/categories/1/')
|
||||
|
||||
resp = resp.click(href='edit')
|
||||
assert resp.forms[0]['name'].value == 'foobar'
|
||||
resp.forms[0]['name'] = 'foobar2'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert 'This name is already used' in resp.text
|
||||
|
||||
resp = resp.forms[0].submit('cancel')
|
||||
assert resp.location == 'http://example.net/backoffice/forms/categories/'
|
||||
|
||||
|
||||
def test_categories_with_formdefs(pub):
|
||||
Category.wipe()
|
||||
category = Category(name='foobar')
|
||||
category.store()
|
||||
|
||||
FormDef.wipe()
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/categories/1/')
|
||||
assert 'form bar' not in resp.text
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form bar'
|
||||
formdef.fields = []
|
||||
formdef.category_id = category.id
|
||||
formdef.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/categories/1/')
|
||||
assert 'form bar' in resp.text
|
||||
assert 'No form associated to this category' not in resp.text
|
||||
|
||||
|
||||
def test_categories_delete(pub):
|
||||
Category.wipe()
|
||||
category = Category(name='foobar')
|
||||
category.store()
|
||||
|
||||
FormDef.wipe()
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/categories/1/')
|
||||
assert 'popup.js' in resp.text
|
||||
|
||||
resp = resp.click(href='delete')
|
||||
resp = resp.forms[0].submit('cancel')
|
||||
assert resp.location == 'http://example.net/backoffice/forms/categories/1/'
|
||||
assert Category.count() == 1
|
||||
|
||||
resp = app.get('/backoffice/forms/categories/1/')
|
||||
resp = resp.click(href='delete')
|
||||
resp = resp.forms[0].submit()
|
||||
assert resp.location == 'http://example.net/backoffice/forms/categories/'
|
||||
resp = resp.follow()
|
||||
assert Category.count() == 0
|
||||
|
||||
|
||||
def test_categories_edit_description(pub):
|
||||
Category.wipe()
|
||||
category = Category(name='foobar')
|
||||
category.description = 'category description'
|
||||
category.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
# this URL is used for editing from the frontoffice, there's no link
|
||||
# pointing to it in the admin.
|
||||
resp = app.get('/backoffice/forms/categories/1/description')
|
||||
assert resp.forms[0]['description'].value == 'category description'
|
||||
resp.forms[0]['description'] = 'updated description'
|
||||
|
||||
# check cancel doesn't save the change
|
||||
resp2 = resp.forms[0].submit('cancel')
|
||||
assert resp2.location == 'http://example.net/backoffice/forms/categories/1/'
|
||||
assert Category.get(1).description == 'category description'
|
||||
|
||||
# check submit does it properly
|
||||
resp2 = resp.forms[0].submit('submit')
|
||||
assert resp2.location == 'http://example.net/backoffice/forms/categories/1/'
|
||||
resp2 = resp2.follow()
|
||||
assert Category.get(1).description == 'updated description'
|
||||
|
||||
|
||||
def test_categories_new_duplicate_name(pub):
|
||||
Category.wipe()
|
||||
category = Category(name='foobar')
|
||||
category.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/categories/')
|
||||
resp = resp.click('New Category')
|
||||
resp.forms[0]['name'] = 'foobar'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert 'This name is already used' in resp.text
|
||||
|
||||
|
||||
def test_categories_reorder(pub):
|
||||
Category.wipe()
|
||||
category = Category(name='foo')
|
||||
category.store()
|
||||
category = Category(name='bar')
|
||||
category.store()
|
||||
category = Category(name='baz')
|
||||
category.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
app.get('/backoffice/forms/categories/update_order?order=1;2;3;')
|
||||
categories = Category.select()
|
||||
Category.sort_by_position(categories)
|
||||
assert [x.id for x in categories] == ['1', '2', '3']
|
||||
|
||||
app.get('/backoffice/forms/categories/update_order?order=3;1;2;0')
|
||||
categories = Category.select()
|
||||
Category.sort_by_position(categories)
|
||||
assert [x.id for x in categories] == ['3', '1', '2']
|
||||
|
||||
|
||||
def test_categories_edit_roles(pub):
|
||||
pub.role_class.wipe()
|
||||
role_a = pub.role_class(name='a')
|
||||
role_a.store()
|
||||
role_b = pub.role_class(name='b')
|
||||
role_b.store()
|
||||
|
||||
Category.wipe()
|
||||
category = Category(name='foobar')
|
||||
category.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/categories/1/edit')
|
||||
|
||||
resp.form['export_roles$element0'] = role_a.id
|
||||
resp = resp.form.submit('export_roles$add_element')
|
||||
resp.form['export_roles$element1'] = role_b.id
|
||||
|
||||
resp.form['statistics_roles$element0'] = role_a.id
|
||||
resp = resp.form.submit('submit')
|
||||
|
||||
category = Category.get(category.id)
|
||||
assert {x.id for x in category.export_roles} == {role_a.id, role_b.id}
|
||||
assert {x.id for x in category.statistics_roles} == {role_a.id}
|
||||
|
||||
resp = app.get('/backoffice/forms/categories/1/edit')
|
||||
assert resp.form['export_roles$element0'].value == role_a.id
|
||||
|
||||
|
||||
def test_categories_export(pub):
|
||||
Category.wipe()
|
||||
category = Category(name='foobar')
|
||||
category.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/categories/1/')
|
||||
resp = resp.click('Export')
|
||||
xml_export = resp.text
|
||||
|
||||
xml_export_fd = io.StringIO(xml_export)
|
||||
imported_category = Category.import_from_xml(xml_export_fd)
|
||||
assert imported_category.name == category.name
|
||||
|
||||
|
||||
def test_categories_import(pub):
|
||||
app = login(get_app(pub))
|
||||
|
||||
Category.wipe()
|
||||
category = Category(name='foobar')
|
||||
category.store()
|
||||
category_xml = ET.tostring(category.export_to_xml(include_id=True))
|
||||
Category.wipe()
|
||||
CardDefCategory.wipe()
|
||||
|
||||
# import to wrong category kind
|
||||
resp = app.get('/backoffice/cards/categories/')
|
||||
resp = resp.click(href='import')
|
||||
resp.forms[0]['file'] = Upload('category.wcs', category_xml)
|
||||
resp = resp.forms[0].submit()
|
||||
assert 'Invalid File' in resp.text
|
||||
assert Category.count() == 0
|
||||
assert CardDefCategory.count() == 0
|
||||
|
||||
# successful import
|
||||
resp = app.get('/backoffice/forms/categories/')
|
||||
resp = resp.click(href='import')
|
||||
resp.forms[0]['file'] = Upload('category.wcs', category_xml)
|
||||
resp = resp.forms[0].submit()
|
||||
assert Category.count() == 1
|
||||
assert {x.slug for x in Category.select()} == {'foobar'}
|
||||
|
||||
# repeat import -> slug change
|
||||
resp = app.get('/backoffice/forms/categories/')
|
||||
resp = resp.click(href='import')
|
||||
resp.forms[0]['file'] = Upload('category.wcs', category_xml)
|
||||
resp = resp.forms[0].submit()
|
||||
assert Category.count() == 2
|
||||
assert {x.slug for x in Category.select()} == {'foobar', 'foobar-2'}
|
||||
|
||||
# cancel
|
||||
resp = app.get('/backoffice/forms/categories/')
|
||||
resp = resp.click(href='import')
|
||||
resp.forms[0]['file'] = Upload('category.wcs', category_xml)
|
||||
resp = resp.forms[0].submit('cancel')
|
||||
assert Category.count() == 2
|
|
@ -1,679 +0,0 @@
|
|||
import io
|
||||
import json
|
||||
import os
|
||||
import zipfile
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from quixote.http_request import Upload as QuixoteUpload
|
||||
|
||||
from wcs import fields
|
||||
from wcs.backoffice.deprecations import DeprecatedElementsDetected, DeprecationsScan
|
||||
from wcs.blocks import BlockDef, BlockdefImportError
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.data_sources import NamedDataSource, NamedDataSourceImportError
|
||||
from wcs.formdef import FormDef, FormdefImportError
|
||||
from wcs.mail_templates import MailTemplate
|
||||
from wcs.qommon.form import UploadedFile
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.wf.create_formdata import Mapping
|
||||
from wcs.wf.export_to_model import ExportToModel
|
||||
from wcs.wf.external_workflow import ExternalWorkflowGlobalAction
|
||||
from wcs.wf.form import WorkflowFormFieldsFormDef
|
||||
from wcs.wf.geolocate import GeolocateWorkflowStatusItem
|
||||
from wcs.wf.jump import JumpWorkflowStatusItem
|
||||
from wcs.wf.notification import SendNotificationWorkflowStatusItem
|
||||
from wcs.wf.redirect_to_url import RedirectToUrlWorkflowStatusItem
|
||||
from wcs.workflows import (
|
||||
Workflow,
|
||||
WorkflowBackofficeFieldsFormDef,
|
||||
WorkflowImportError,
|
||||
WorkflowVariablesFieldsFormDef,
|
||||
)
|
||||
from wcs.wscalls import NamedWsCall, NamedWsCallImportError
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import create_superuser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub(request):
|
||||
pub = create_temporary_pub()
|
||||
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
pub.set_app_dir(req)
|
||||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.cfg['language'] = {'language': 'en'}
|
||||
pub.write_cfg()
|
||||
|
||||
if os.path.exists(os.path.join(pub.app_dir, 'deprecations.json')):
|
||||
os.remove(os.path.join(pub.app_dir, 'deprecations.json'))
|
||||
|
||||
BlockDef.wipe()
|
||||
CardDef.wipe()
|
||||
FormDef.wipe()
|
||||
MailTemplate.wipe()
|
||||
NamedDataSource.wipe()
|
||||
NamedWsCall.wipe()
|
||||
Workflow.wipe()
|
||||
|
||||
return pub
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_no_deprecations(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
# first time, it's a redirect to the scanning job
|
||||
resp = app.get('/backoffice/studio/deprecations/', status=302)
|
||||
resp = resp.follow()
|
||||
resp = resp.click('Go to deprecation report')
|
||||
# second time, the page stays on
|
||||
resp = app.get('/backoffice/studio/deprecations/', status=200)
|
||||
assert 'No deprecated items were found on this site.' in resp.text
|
||||
|
||||
|
||||
def test_deprecations(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foobar'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='1', label='page1', condition={'type': 'python', 'value': 'True'}),
|
||||
fields.StringField(id='2', label='python_prefill', prefill={'type': 'formula', 'value': '1 + 2'}),
|
||||
fields.StringField(
|
||||
id='3', label='ezt_prefill', prefill={'type': 'string', 'value': '[form_var_test]'}
|
||||
),
|
||||
fields.StringField(id='4', label='jsonp_data', data_source={'type': 'jsonp', 'value': 'xxx'}),
|
||||
fields.StringField(id='5', label='ezt_in_datasource', data_source={'type': 'json', 'value': '[xxx]'}),
|
||||
fields.CommentField(id='6', label='[ezt] in label'),
|
||||
fields.CommentField(id='7', label='{{script.usage}} in template'),
|
||||
fields.PageField(
|
||||
id='10',
|
||||
label='page2',
|
||||
post_conditions=[
|
||||
{'condition': {'type': 'python', 'value': 'False'}, 'error_message': 'You shall not pass.'}
|
||||
],
|
||||
),
|
||||
fields.TableField(id='8', label='table field'),
|
||||
fields.RankedItemsField(id='9', label='ranked field'),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
workflow = Workflow(name='test')
|
||||
workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow)
|
||||
workflow.backoffice_fields_formdef.fields = [
|
||||
fields.TableField(id='bo1', label='table field'),
|
||||
]
|
||||
workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow)
|
||||
workflow.variables_formdef.fields = [
|
||||
fields.TableField(id='wfvar1', label='other table field'),
|
||||
]
|
||||
st0 = workflow.add_status('Status0', 'st0')
|
||||
|
||||
display = st0.add_action('displaymsg')
|
||||
display.message = 'message with [ezt] info'
|
||||
|
||||
wscall = st0.add_action('webservice_call', id='_wscall')
|
||||
wscall.varname = 'xxx'
|
||||
wscall.url = 'http://remote.example.net/xml'
|
||||
wscall.post_data = {'str': 'abcd', 'evalme': '=form_number'}
|
||||
|
||||
sendsms = st0.add_action('sendsms', id='_sendsms')
|
||||
sendsms.to = 'xxx'
|
||||
sendsms.condition = {'type': 'python', 'value': 'True'}
|
||||
sendsms.parent = st0
|
||||
st0.items.append(sendsms)
|
||||
|
||||
item = st0.add_action('set-backoffice-fields', id='_item')
|
||||
item.fields = [{'field_id': 'bo1', 'value': '=form_var_foo'}]
|
||||
|
||||
create_formdata = st0.add_action('create_formdata', id='_create_formdata')
|
||||
create_formdata.varname = 'resubmitted'
|
||||
create_formdata.mappings = [
|
||||
Mapping(field_id='0', expression='=form_var_toto_string'),
|
||||
]
|
||||
|
||||
item = st0.add_action('update_user_profile', id='_item2')
|
||||
item.fields = [{'field_id': '__email', 'value': '=form_var_foo'}]
|
||||
|
||||
sendmail = st0.add_action('sendmail', id='_sendmail')
|
||||
sendmail.to = ['=plop']
|
||||
|
||||
sendmail = st0.add_action('sendmail', id='_sendmail2')
|
||||
sendmail.attachments = ['python']
|
||||
|
||||
display_form = st0.add_action('form', id='_x')
|
||||
display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
|
||||
display_form.formdef.fields.append(
|
||||
fields.StringField(id='0', label='Test', prefill={'type': 'formula', 'value': '1 + 2'})
|
||||
)
|
||||
|
||||
export_to = st0.add_action('export_to_model', id='_export_to')
|
||||
export_to.convert_to_pdf = False
|
||||
export_to.label = 'create doc'
|
||||
upload = QuixoteUpload('/foo/test.rtf', content_type='application/rtf')
|
||||
upload.fp = io.BytesIO()
|
||||
upload.fp.write(b'HELLO WORLD')
|
||||
upload.fp.seek(0)
|
||||
export_to.model_file = UploadedFile(pub.app_dir, None, upload)
|
||||
export_to.by = ['_submitter']
|
||||
|
||||
timeout_jump = st0.add_action('jump')
|
||||
timeout_jump.timeout = '213'
|
||||
timeout_jump.mode = 'timeout'
|
||||
timeout_jump.condition = {'type': 'python', 'value': 'True'}
|
||||
|
||||
for klass in (
|
||||
ExportToModel,
|
||||
ExternalWorkflowGlobalAction,
|
||||
GeolocateWorkflowStatusItem,
|
||||
JumpWorkflowStatusItem,
|
||||
SendNotificationWorkflowStatusItem,
|
||||
RedirectToUrlWorkflowStatusItem,
|
||||
):
|
||||
action = klass()
|
||||
action.parent = st0
|
||||
st0.items.append(action)
|
||||
|
||||
st0.add_action('aggregationemail')
|
||||
|
||||
global_action = workflow.add_global_action('global')
|
||||
trigger = global_action.append_trigger('timeout')
|
||||
trigger.anchor = 'python'
|
||||
trigger.anchor_expression = 'form_var_date'
|
||||
|
||||
workflow.store()
|
||||
|
||||
data_source = NamedDataSource(name='ds_python')
|
||||
data_source.data_source = {'type': 'formula', 'value': repr([('1', 'un'), ('2', 'deux')])}
|
||||
data_source.store()
|
||||
data_source = NamedDataSource(name='ds_jsonp')
|
||||
data_source.data_source = {'type': 'jsonp', 'value': 'xxx'}
|
||||
data_source.store()
|
||||
data_source = NamedDataSource(name='ds_csv')
|
||||
data_source.data_source = {'type': 'json', 'value': 'http://example.net/csvdatasource/plop/test'}
|
||||
data_source.store()
|
||||
|
||||
NamedWsCall.wipe()
|
||||
wscall = NamedWsCall()
|
||||
wscall.name = 'Hello'
|
||||
wscall.request = {'url': 'http://example.net', 'qs_data': {'a': '=1+2'}}
|
||||
wscall.store()
|
||||
|
||||
wscall = NamedWsCall()
|
||||
wscall.name = 'Hello CSV'
|
||||
wscall.request = {'url': 'http://example.net/csvdatasource/plop/test'}
|
||||
wscall.store()
|
||||
|
||||
wscall = NamedWsCall()
|
||||
wscall.name = 'Hello json data store'
|
||||
wscall.request = {'url': 'http://example.net/jsondatastore/plop'}
|
||||
wscall.store()
|
||||
|
||||
MailTemplate.wipe()
|
||||
mail_template1 = MailTemplate()
|
||||
mail_template1.name = 'Hello1'
|
||||
mail_template1.subject = 'plop'
|
||||
mail_template1.body = 'plop'
|
||||
mail_template1.attachments = ['form_attachments.plop']
|
||||
mail_template1.store()
|
||||
mail_template2 = MailTemplate()
|
||||
mail_template2.name = 'Hello2'
|
||||
mail_template2.subject = 'plop'
|
||||
mail_template2.body = 'plop [ezt] plop'
|
||||
mail_template2.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/studio/deprecations/', status=302)
|
||||
resp = resp.follow()
|
||||
resp = resp.click('Go to deprecation report')
|
||||
|
||||
assert [x.text for x in resp.pyquery('.section--ezt li a')] == [
|
||||
'foobar / Field "ezt_prefill"',
|
||||
'foobar / Field "ezt_in_datasource"',
|
||||
'foobar / Field "[ezt] in label"',
|
||||
'test / Alert',
|
||||
'Mail Template "Hello2"',
|
||||
]
|
||||
assert [x.text for x in resp.pyquery('.section--jsonp li a')] == [
|
||||
'foobar / Field "jsonp_data"',
|
||||
'Data source "ds_jsonp"',
|
||||
]
|
||||
assert [x.text for x in resp.pyquery('.section--python-data-source li a')] == ['Data source "ds_python"']
|
||||
assert [x.text for x in resp.pyquery('.section--python-condition li a')] == [
|
||||
'foobar / Field "page1"',
|
||||
'foobar / Field "page2"',
|
||||
'test / SMS',
|
||||
'test / Automatic Jump',
|
||||
]
|
||||
assert [x.text for x in resp.pyquery('.section--python-condition li.important a')] == [
|
||||
'test / Automatic Jump',
|
||||
]
|
||||
assert [x.text for x in resp.pyquery('.section--python-prefill li a')] == [
|
||||
'foobar / Field "python_prefill"',
|
||||
'Form action in workflow "test" / Field "Test"',
|
||||
]
|
||||
assert [x.text for x in resp.pyquery('.section--python-expression li a')] == [
|
||||
'test / Webservice',
|
||||
'test / Backoffice Data',
|
||||
'test / New Form Creation',
|
||||
'test / User Profile Update',
|
||||
'test / Email',
|
||||
'test / Email',
|
||||
'test / trigger in global',
|
||||
'Webservice "Hello"',
|
||||
'Mail Template "Hello1"',
|
||||
]
|
||||
assert [x.text for x in resp.pyquery('.section--script li a')] == [
|
||||
'foobar / Field "{{script.usage}} in template"'
|
||||
]
|
||||
assert [x.text for x in resp.pyquery('.section--rtf li a')] == [
|
||||
'test / Document Creation',
|
||||
]
|
||||
assert [x.text for x in resp.pyquery('.section--fields li a')] == [
|
||||
'foobar / Field "table field"',
|
||||
'foobar / Field "ranked field"',
|
||||
'Options of workflow "test" / Field "other table field"',
|
||||
'Backoffice fields of workflow "test" / Field "table field"',
|
||||
]
|
||||
assert [x.text for x in resp.pyquery('.section--actions li a')] == [
|
||||
'test / Daily Summary Email',
|
||||
]
|
||||
assert [x.text for x in resp.pyquery('.section--csv-connector li a')] == [
|
||||
'Data source "ds_csv"',
|
||||
'Webservice "Hello CSV"',
|
||||
]
|
||||
assert [x.text for x in resp.pyquery('.section--json-data-store li a')] == [
|
||||
'Webservice "Hello json data store"',
|
||||
]
|
||||
# check all links are ok
|
||||
for link in resp.pyquery('.section li a'):
|
||||
resp.click(href=link.attrib['href'], index=0)
|
||||
|
||||
|
||||
def test_deprecations_choice_label(pub):
|
||||
MailTemplate.wipe()
|
||||
|
||||
# check choice labels are not considered as EZT
|
||||
workflow = Workflow(name='test')
|
||||
st0 = workflow.add_status('Status0', 'st0')
|
||||
|
||||
accept = st0.add_action('choice', id='_choice')
|
||||
accept.label = '[test] action'
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
assert not job.report_lines
|
||||
|
||||
|
||||
def test_deprecations_skip_invalid_ezt(pub):
|
||||
workflow = Workflow(name='test')
|
||||
st0 = workflow.add_status('Status0', 'st0')
|
||||
|
||||
display = st0.add_action('displaymsg')
|
||||
display.message = 'message with invalid [if-any] ezt'
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
assert not job.report_lines
|
||||
|
||||
|
||||
def test_deprecations_ignore_ezt_looking_tag(pub):
|
||||
workflow = Workflow(name='test')
|
||||
st0 = workflow.add_status('Status0', 'st0')
|
||||
sendmail = st0.add_action('sendmail')
|
||||
sendmail.subject = '[REMINDER] your appointment'
|
||||
workflow.store()
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
assert not job.report_lines
|
||||
|
||||
sendmail.subject = '[reminder]'
|
||||
workflow.store()
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
assert job.report_lines
|
||||
|
||||
sendmail.subject = '[if-any plop]test[end]'
|
||||
workflow.store()
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
assert job.report_lines
|
||||
|
||||
|
||||
def test_deprecations_field_limits(pub):
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foobar'
|
||||
formdef.fields = [fields.StringField(id=str(x), label=f'field{x}') for x in range(450)]
|
||||
formdef.store()
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
assert len(job.report_lines) == 1
|
||||
assert job.report_lines[0]['category'] == 'field-limits'
|
||||
|
||||
|
||||
def test_deprecations_cronjob(pub):
|
||||
assert not os.path.exists(os.path.join(pub.app_dir, 'deprecations.json'))
|
||||
pub.update_deprecations_report()
|
||||
assert os.path.exists(os.path.join(pub.app_dir, 'deprecations.json'))
|
||||
|
||||
|
||||
def test_deprecations_document_models(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
workflow = Workflow(name='test')
|
||||
st0 = workflow.add_status('Status0', 'st0')
|
||||
export_to = st0.add_action('export_to_model')
|
||||
export_to.convert_to_pdf = False
|
||||
export_to.label = 'create doc'
|
||||
upload = QuixoteUpload('test.rtf', content_type='text/rtf')
|
||||
upload.fp = io.BytesIO()
|
||||
upload.fp.write(b'{\\rtf foo [form_var_plop] bar')
|
||||
upload.fp.seek(0)
|
||||
export_to.model_file = UploadedFile(pub.app_dir, None, upload)
|
||||
export_to.by = ['_submitter']
|
||||
|
||||
export_to2 = st0.add_action('export_to_model')
|
||||
export_to2.convert_to_pdf = False
|
||||
export_to2.label = 'create doc2'
|
||||
upload = QuixoteUpload('test.odt', content_type='application/vnd.oasis.opendocument.text')
|
||||
upload.fp = io.BytesIO()
|
||||
with zipfile.ZipFile(upload.fp, mode='w') as zout:
|
||||
content = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<office:document-content
|
||||
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
|
||||
xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"
|
||||
xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
office:version="1.2">
|
||||
<office:body>
|
||||
<office:text>
|
||||
<text:sequence-decls>
|
||||
<text:sequence-decl text:display-outline-level="0" text:name="Illustration"/>
|
||||
<text:sequence-decl text:display-outline-level="0" text:name="Table"/>
|
||||
<text:sequence-decl text:display-outline-level="0" text:name="Text"/>
|
||||
<text:sequence-decl text:display-outline-level="0" text:name="Drawing"/>
|
||||
</text:sequence-decls>
|
||||
<text:user-field-decls>
|
||||
<text:user-field-decl office:value-type="string" office:string-value="{{ form_name }}"/>
|
||||
</text:user-field-decls>
|
||||
<text:p text:style-name="P1">Hello.</text:p>
|
||||
<text:p text:style-name="P2">
|
||||
<draw:frame draw:style-name="fr1" draw:name="=form_var_image_raw"
|
||||
text:anchor-type="paragraph" svg:width="1.764cm" svg:height="1.764cm" draw:z-index="0">
|
||||
<draw:image xlink:href="Pictures/10000000000000320000003276E9D46581B55C88.jpg"
|
||||
xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/>
|
||||
</draw:frame>
|
||||
</text:p>
|
||||
</office:text>
|
||||
</office:body>
|
||||
</office:document-content>
|
||||
'''
|
||||
zout.writestr('content.xml', content)
|
||||
upload.fp.seek(0)
|
||||
export_to2.model_file = UploadedFile(pub.app_dir, None, upload)
|
||||
export_to2.by = ['_submitter']
|
||||
workflow.store()
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
assert job.report_lines == [
|
||||
{
|
||||
'category': 'ezt',
|
||||
'location_label': 'test / Document Creation',
|
||||
'source': 'workflow:1',
|
||||
'url': 'http://example.net/backoffice/workflows/1/status/st0/items/1/',
|
||||
},
|
||||
{
|
||||
'category': 'rtf',
|
||||
'location_label': 'test / Document Creation',
|
||||
'source': 'workflow:1',
|
||||
'url': 'http://example.net/backoffice/workflows/1/status/st0/items/1/',
|
||||
},
|
||||
{
|
||||
'category': 'python-expression',
|
||||
'location_label': 'test / Document Creation',
|
||||
'source': 'workflow:1',
|
||||
'url': 'http://example.net/backoffice/workflows/1/status/st0/items/2/',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_deprecations_inspect_pages(pub):
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foobar'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='1', label='page1', condition={'type': 'python', 'value': 'True'}),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.StringField(id='2', label='python_prefill', prefill={'type': 'formula', 'value': '1 + 2'}),
|
||||
]
|
||||
block.store()
|
||||
|
||||
workflow = Workflow(name='test')
|
||||
workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow)
|
||||
workflow.backoffice_fields_formdef.fields = [
|
||||
fields.TableField(id='bo1', label='table field'),
|
||||
]
|
||||
st0 = workflow.add_status('Status0', 'st0')
|
||||
display = st0.add_action('displaymsg')
|
||||
display.message = 'message with [ezt] info'
|
||||
workflow.store()
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get(formdef.get_admin_url() + 'inspect')
|
||||
assert 'Deprecations' in resp.text
|
||||
|
||||
resp = app.get(block.get_admin_url() + 'inspect')
|
||||
assert 'Deprecations' in resp.text
|
||||
|
||||
resp = app.get(workflow.get_admin_url() + 'inspect')
|
||||
assert 'Deprecations' in resp.text
|
||||
|
||||
# check there's no deprecation tab in snapshots
|
||||
snapshot = pub.snapshot_class.get_latest('formdef', formdef.id)
|
||||
resp = app.get(formdef.get_admin_url() + f'history/{snapshot.id}/inspect')
|
||||
assert 'Deprecations' not in resp.text
|
||||
|
||||
snapshot = pub.snapshot_class.get_latest('block', block.id)
|
||||
resp = app.get(block.get_admin_url() + f'history/{snapshot.id}/inspect')
|
||||
assert 'Deprecations' not in resp.text
|
||||
|
||||
snapshot = pub.snapshot_class.get_latest('workflow', workflow.id)
|
||||
resp = app.get(workflow.get_admin_url() + f'history/{snapshot.id}/inspect')
|
||||
assert 'Deprecations' not in resp.text
|
||||
|
||||
# check there's no deprecation tab if there's nothing deprecated
|
||||
formdef.fields[0].condition = None
|
||||
formdef.store()
|
||||
|
||||
block.fields[0].prefill = None
|
||||
block.store()
|
||||
|
||||
workflow.backoffice_fields_formdef = None
|
||||
display.message = 'message with {{django}} info'
|
||||
workflow.store()
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
|
||||
resp = app.get(formdef.get_admin_url() + 'inspect')
|
||||
assert 'Deprecations' not in resp.text
|
||||
|
||||
resp = app.get(block.get_admin_url() + 'inspect')
|
||||
assert 'Deprecations' not in resp.text
|
||||
|
||||
resp = app.get(workflow.get_admin_url() + 'inspect')
|
||||
assert 'Deprecations' not in resp.text
|
||||
|
||||
|
||||
def test_deprecations_inspect_pages_old_format(pub):
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foobar'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='1', label='page1', condition={'type': 'python', 'value': 'True'}),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.execute()
|
||||
|
||||
with open(os.path.join(pub.app_dir, 'deprecations.json')) as f:
|
||||
deprecations_json = json.loads(f.read())
|
||||
|
||||
del deprecations_json['report_lines'][0]['source']
|
||||
|
||||
with open(os.path.join(pub.app_dir, 'deprecations.json'), 'w') as f:
|
||||
f.write(json.dumps(deprecations_json))
|
||||
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get(formdef.get_admin_url() + 'inspect')
|
||||
assert 'Deprecations' not in resp.text
|
||||
|
||||
resp = app.get('/backoffice/studio/deprecations/')
|
||||
assert resp.pyquery('.section--python-condition li a')
|
||||
|
||||
|
||||
def test_deprecations_on_import(pub):
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foobar'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='1', label='page1', condition={'type': 'python', 'value': 'True'}),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
blockdef = BlockDef()
|
||||
blockdef.name = 'foobar'
|
||||
blockdef.fields = [
|
||||
fields.StringField(id='2', label='python_prefill', prefill={'type': 'formula', 'value': '1 + 2'}),
|
||||
]
|
||||
blockdef.store()
|
||||
|
||||
workflow = Workflow(name='test')
|
||||
st0 = workflow.add_status('Status0', 'st0')
|
||||
sendsms = st0.add_action('sendsms', id='_sendsms')
|
||||
sendsms.to = 'xxx'
|
||||
sendsms.condition = {'type': 'python', 'value': 'True'}
|
||||
sendsms.parent = st0
|
||||
st0.items.append(sendsms)
|
||||
workflow.store()
|
||||
|
||||
data_source = NamedDataSource(name='ds_python')
|
||||
data_source.data_source = {'type': 'formula', 'value': repr([('1', 'un'), ('2', 'deux')])}
|
||||
data_source.store()
|
||||
|
||||
wscall = NamedWsCall()
|
||||
wscall.name = 'Hello'
|
||||
wscall.request = {'url': 'http://example.net', 'qs_data': {'a': '=1+2'}}
|
||||
wscall.store()
|
||||
|
||||
mail_template = MailTemplate() # no python expression in mail templates
|
||||
mail_template.name = 'Hello2'
|
||||
mail_template.subject = 'plop'
|
||||
mail_template.body = 'plop [ezt] plop'
|
||||
mail_template.store()
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.check_deprecated_elements_in_object(formdef)
|
||||
formdef_xml = formdef.export_to_xml()
|
||||
FormDef.import_from_xml_tree(formdef_xml)
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.check_deprecated_elements_in_object(blockdef)
|
||||
blockdef_xml = blockdef.export_to_xml()
|
||||
BlockDef.import_from_xml_tree(blockdef_xml)
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.check_deprecated_elements_in_object(workflow)
|
||||
workflow_xml = workflow.export_to_xml()
|
||||
Workflow.import_from_xml_tree(workflow_xml)
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.check_deprecated_elements_in_object(data_source)
|
||||
data_source_xml = data_source.export_to_xml()
|
||||
NamedDataSource.import_from_xml_tree(data_source_xml)
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.check_deprecated_elements_in_object(wscall)
|
||||
wscall_xml = wscall.export_to_xml()
|
||||
NamedWsCall.import_from_xml_tree(wscall_xml)
|
||||
|
||||
job = DeprecationsScan()
|
||||
job.check_deprecated_elements_in_object(mail_template)
|
||||
mail_template_xml = mail_template.export_to_xml()
|
||||
MailTemplate.import_from_xml_tree(mail_template_xml)
|
||||
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'forbid-new-python-expressions', 'true')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
job = DeprecationsScan()
|
||||
with pytest.raises(DeprecatedElementsDetected) as excinfo:
|
||||
job.check_deprecated_elements_in_object(formdef)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
with pytest.raises(FormdefImportError) as excinfo:
|
||||
FormDef.import_from_xml_tree(formdef_xml, check_deprecated=True)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
|
||||
job = DeprecationsScan()
|
||||
with pytest.raises(DeprecatedElementsDetected) as excinfo:
|
||||
job.check_deprecated_elements_in_object(blockdef)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
with pytest.raises(BlockdefImportError) as excinfo:
|
||||
BlockDef.import_from_xml_tree(blockdef_xml, check_deprecated=True)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
|
||||
job = DeprecationsScan()
|
||||
with pytest.raises(DeprecatedElementsDetected) as excinfo:
|
||||
job.check_deprecated_elements_in_object(workflow)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
with pytest.raises(WorkflowImportError) as excinfo:
|
||||
Workflow.import_from_xml_tree(workflow_xml, check_deprecated=True)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
|
||||
job = DeprecationsScan()
|
||||
with pytest.raises(DeprecatedElementsDetected) as excinfo:
|
||||
job.check_deprecated_elements_in_object(data_source)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
with pytest.raises(NamedDataSourceImportError) as excinfo:
|
||||
NamedDataSource.import_from_xml_tree(data_source_xml, check_deprecated=True)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
|
||||
job = DeprecationsScan()
|
||||
with pytest.raises(DeprecatedElementsDetected) as excinfo:
|
||||
job.check_deprecated_elements_in_object(wscall)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
with pytest.raises(NamedWsCallImportError) as excinfo:
|
||||
NamedWsCall.import_from_xml_tree(wscall_xml, check_deprecated=True)
|
||||
assert str(excinfo.value) == 'Python expression detected'
|
||||
|
||||
# no python expressions
|
||||
job = DeprecationsScan()
|
||||
job.check_deprecated_elements_in_object(mail_template)
|
||||
MailTemplate.import_from_xml_tree(mail_template_xml)
|
||||
|
||||
# check that DeprecationsScan is not run on object load
|
||||
with mock.patch(
|
||||
'wcs.backoffice.deprecations.DeprecationsScan.check_deprecated_elements_in_object'
|
||||
) as check:
|
||||
NamedDataSource.get(data_source.id)
|
||||
assert check.call_args_list == []
|
|
@ -1,423 +0,0 @@
|
|||
import io
|
||||
import zipfile
|
||||
|
||||
import pytest
|
||||
from webtest import Upload
|
||||
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.categories import Category
|
||||
from wcs.fields import ItemField, PageField, StringField
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.i18n import TranslatableMessage
|
||||
from wcs.mail_templates import MailTemplate
|
||||
from wcs.qommon import ods
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.sql import Equal
|
||||
from wcs.workflows import Workflow
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import create_superuser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub():
|
||||
pub = create_temporary_pub()
|
||||
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
pub.set_app_dir(req)
|
||||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.cfg['language'] = {'language': 'en', 'multilinguism': True, 'languages': ['en', 'fr']}
|
||||
pub.write_cfg()
|
||||
|
||||
TranslatableMessage.do_table() # update table with selected languages
|
||||
|
||||
TranslatableMessage.wipe()
|
||||
Workflow.wipe()
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
Category.wipe()
|
||||
CardDef.wipe()
|
||||
MailTemplate.wipe()
|
||||
|
||||
return pub
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_i18n_link_on_studio_page(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/studio/')
|
||||
assert '../i18n/' in resp.text
|
||||
pub.cfg['language']['multilinguism'] = False
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/studio/')
|
||||
assert '../i18n/' not in resp.text
|
||||
app.get('/backoffice/i18n/', status=404)
|
||||
|
||||
|
||||
def test_i18n_page(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
workflow = Workflow(name='workflow')
|
||||
st = workflow.add_status('First Status')
|
||||
sendmail = st.add_action('sendmail')
|
||||
sendmail.to = ['_submitter']
|
||||
sendmail.subject = 'Email Subject'
|
||||
sendmail.body = 'Email body'
|
||||
editable = st.add_action('editable')
|
||||
editable.label = 'Edit Button'
|
||||
workflow.add_global_action('Global Manual')
|
||||
action2 = workflow.add_global_action('Global No Trigger')
|
||||
action2.triggers = []
|
||||
workflow.store()
|
||||
|
||||
workflow2 = Workflow(name='second workflow')
|
||||
workflow2.add_status('Other Status')
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.fields = [
|
||||
PageField(
|
||||
id='0',
|
||||
label='page field',
|
||||
post_conditions=[
|
||||
{'condition': {'type': 'django', 'value': 'blah'}, 'error_message': 'page error message'},
|
||||
],
|
||||
),
|
||||
StringField(id='1', label='text field'),
|
||||
StringField(
|
||||
id='2',
|
||||
label='text field',
|
||||
validation={'type': 'django', 'value': 'False', 'error_message': 'Custom Error'},
|
||||
),
|
||||
ItemField(id='3', label='list field', items=['first', 'second', 'third']),
|
||||
]
|
||||
formdef.workflow = workflow
|
||||
formdef.store()
|
||||
|
||||
block = BlockDef(name='test')
|
||||
# check strings will be stripped
|
||||
block.fields = [StringField(id='1', label='text field ')]
|
||||
block.post_conditions = [
|
||||
{'condition': {'type': 'django', 'value': 'blah1'}, 'error_message': 'block post condition error'},
|
||||
]
|
||||
block.store()
|
||||
|
||||
carddef = CardDef()
|
||||
carddef.name = 'card test'
|
||||
carddef.store()
|
||||
|
||||
category = Category(name='Category Name')
|
||||
category.store()
|
||||
|
||||
mail_template = MailTemplate(name='test mail template')
|
||||
mail_template.subject = 'test subject'
|
||||
mail_template.body = 'test body'
|
||||
mail_template.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
# first time goes to scanning
|
||||
resp = app.get('/backoffice/i18n/', status=302)
|
||||
resp = resp.follow()
|
||||
resp = resp.click('Go to multilinguism page')
|
||||
# second time, the page stays on
|
||||
resp = app.get('/backoffice/i18n/', status=200)
|
||||
|
||||
# relaunch scan
|
||||
resp = resp.click('Rescan')
|
||||
resp = resp.follow()
|
||||
resp = resp.click('Go to multilinguism page')
|
||||
|
||||
# check 'text field' only appears one
|
||||
assert TranslatableMessage.count([Equal('string', 'text field')]) == 1
|
||||
|
||||
# check page post condition
|
||||
assert TranslatableMessage.count([Equal('string', 'page error message')]) == 1
|
||||
|
||||
# check global action name appears only if there's a manual trigger
|
||||
assert TranslatableMessage.count([Equal('string', 'Global Manual')]) == 1
|
||||
assert TranslatableMessage.count([Equal('string', 'Global No Trigger')]) == 0
|
||||
|
||||
# check edit button label
|
||||
assert TranslatableMessage.count([Equal('string', 'Edit Button')]) == 1
|
||||
|
||||
# check custom validation message
|
||||
assert TranslatableMessage.count([Equal('string', 'Custom Error')]) == 1
|
||||
|
||||
# check block post condition
|
||||
assert TranslatableMessage.count([Equal('string', 'block post condition error')]) == 1
|
||||
|
||||
# check table
|
||||
assert resp.pyquery('tr').length == TranslatableMessage.count()
|
||||
|
||||
# check filters
|
||||
assert resp.form['lang'].value == 'fr'
|
||||
assert [x[2] for x in resp.form['formdef'].options] == [
|
||||
'All forms and card models',
|
||||
'test title',
|
||||
'card test',
|
||||
]
|
||||
resp.form['formdef'] = 'cards/1'
|
||||
resp = resp.form.submit()
|
||||
assert resp.pyquery('tr').length == 1
|
||||
assert {x.text for x in resp.pyquery('tr td:first-child')} == {'card test'}
|
||||
|
||||
# check filtering on a formdef/carddef outputs related workflow strings
|
||||
resp.form['formdef'] = 'forms/1'
|
||||
resp = resp.form.submit()
|
||||
assert resp.pyquery('tr').length == 14
|
||||
assert 'test title' in {x.text for x in resp.pyquery('tr td:first-child')}
|
||||
assert 'Global Manual' in {x.text for x in resp.pyquery('tr td:first-child')}
|
||||
assert 'second workflow' not in {x.text for x in resp.pyquery('tr td:first-child')}
|
||||
|
||||
resp.form['formdef'] = ''
|
||||
resp.form['q'] = 'Email'
|
||||
resp = resp.form.submit()
|
||||
assert resp.pyquery('tr').length == 2 # (email subject, email body)
|
||||
assert {x.text for x in resp.pyquery('tr td:first-child')} == {'Email body', 'Email Subject'}
|
||||
|
||||
# translate a message
|
||||
msg = TranslatableMessage.select([Equal('string', 'Email body')])[0]
|
||||
resp = resp.click('edit', href='/%s/' % msg.id)
|
||||
resp = resp.form.submit('cancel').follow()
|
||||
resp = resp.click('edit', href='/%s/' % msg.id)
|
||||
assert resp.pyquery('.i18n-orig-string').text() == 'Email body'
|
||||
resp.form['translation'] = 'Texte du courriel'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
msg = TranslatableMessage.get(msg.id)
|
||||
assert msg.string_fr == 'Texte du courriel'
|
||||
|
||||
# go back
|
||||
resp = resp.click('edit', href='/%s/' % msg.id)
|
||||
assert resp.form['translation'].value == 'Texte du courriel'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
|
||||
# 404 pages
|
||||
resp = app.get('/backoffice/i18n/fr/%s/' % msg.id, status=200)
|
||||
resp = app.get('/backoffice/i18n/de/%s/' % msg.id, status=404)
|
||||
resp = app.get('/backoffice/i18n/fr/%s000/' % msg.id, status=404)
|
||||
|
||||
|
||||
def test_i18n_export(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.fields = [
|
||||
StringField(id='1', label='text field'),
|
||||
ItemField(id='2', label='list field', items=['first', 'second', 'third']),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
# go and scan
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/i18n/', status=302).follow()
|
||||
resp = resp.click('Go to multilinguism page')
|
||||
|
||||
resp = resp.click('Export')
|
||||
resp = resp.form.submit('cancel').follow()
|
||||
resp = resp.click('Export')
|
||||
resp.form['format'] = 'ods'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
resp = resp.click('Download Export')
|
||||
assert resp.content_type == 'application/vnd.oasis.opendocument.spreadsheet'
|
||||
|
||||
with zipfile.ZipFile(io.BytesIO(resp.body)) as zipf:
|
||||
content = zipf.read('content.xml')
|
||||
assert b'>text field<' in content
|
||||
assert b'>list field<' in content
|
||||
|
||||
resp = app.get('/backoffice/i18n/')
|
||||
resp = resp.click('Export')
|
||||
resp.form['format'] = 'xliff'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
resp = resp.click('Download Export')
|
||||
assert resp.content_type == 'text/xml'
|
||||
assert b'>text field<' in resp.body
|
||||
assert b'>list field<' in resp.body
|
||||
|
||||
# check filtered strings
|
||||
resp = app.get('/backoffice/i18n/')
|
||||
resp.form['q'] = 'list'
|
||||
resp = resp.form.submit('submit')
|
||||
resp = resp.click('Export')
|
||||
resp.form['format'] = 'xliff'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
resp = resp.click('Download Export')
|
||||
assert resp.content_type == 'text/xml'
|
||||
assert b'>text field<' not in resp.body
|
||||
assert b'>list field<' in resp.body
|
||||
|
||||
|
||||
def test_i18n_import(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.fields = [
|
||||
StringField(id='1', label='text field'),
|
||||
ItemField(id='2', label='list field', items=['first', 'second', 'third']),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
# go and scan
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/i18n/', status=302).follow()
|
||||
resp = resp.click('Go to multilinguism page')
|
||||
|
||||
resp = resp.click('Import')
|
||||
resp = resp.form.submit('cancel').follow()
|
||||
resp = resp.click('Import')
|
||||
resp.forms[0]['file'] = Upload(
|
||||
'test.xliff',
|
||||
b'''
|
||||
<xliff:xliff xmlns:xliff="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="fr">
|
||||
<xliff:file id="f1">
|
||||
<xliff:file id="1">
|
||||
<xliff:segment>
|
||||
<xliff:source>text field</xliff:source>
|
||||
<xliff:target />
|
||||
</xliff:segment>
|
||||
<xliff:segment>
|
||||
<xliff:source>list field</xliff:source>
|
||||
<xliff:target>champ liste</xliff:target>
|
||||
</xliff:segment>
|
||||
<xliff:segment>
|
||||
<xliff:source>other text</xliff:source>
|
||||
<xliff:target>autre texte</xliff:target>
|
||||
</xliff:segment>
|
||||
</xliff:file>
|
||||
</xliff:file>
|
||||
</xliff:xliff>
|
||||
''',
|
||||
'text/xml',
|
||||
)
|
||||
resp = resp.form.submit('submit').follow()
|
||||
|
||||
assert TranslatableMessage.count([Equal('string', 'text field')]) == 1
|
||||
assert TranslatableMessage.count([Equal('string', 'list field')]) == 1
|
||||
assert TranslatableMessage.count([Equal('string', 'other text')]) == 1
|
||||
assert TranslatableMessage.select([Equal('string', 'list field')])[0].string_fr == 'champ liste'
|
||||
assert TranslatableMessage.select([Equal('string', 'other text')])[0].string_fr == 'autre texte'
|
||||
|
||||
TranslatableMessage.wipe()
|
||||
workbook = ods.Workbook(encoding='utf-8')
|
||||
ws = workbook.add_sheet('')
|
||||
ws.write(0, 0, 'list field')
|
||||
ws.write(0, 1, 'champ liste')
|
||||
ws.write(1, 0, 'other text')
|
||||
ws.write(1, 1, 'autre texte')
|
||||
output = io.BytesIO()
|
||||
workbook.save(output)
|
||||
|
||||
resp = app.get('/backoffice/i18n/', status=302).follow()
|
||||
resp = resp.click('Go to multilinguism page')
|
||||
resp = resp.click('Import')
|
||||
|
||||
resp.forms[0]['file'] = Upload(
|
||||
'test.ods', output.getvalue(), 'application/vnd.oasis.opendocument.spreadsheet'
|
||||
)
|
||||
resp = resp.form.submit('submit').follow()
|
||||
|
||||
assert TranslatableMessage.count([Equal('string', 'text field')]) == 1
|
||||
assert TranslatableMessage.count([Equal('string', 'list field')]) == 1
|
||||
assert TranslatableMessage.count([Equal('string', 'other text')]) == 1
|
||||
assert TranslatableMessage.select([Equal('string', 'list field')])[0].string_fr == 'champ liste'
|
||||
assert TranslatableMessage.select([Equal('string', 'other text')])[0].string_fr == 'autre texte'
|
||||
|
||||
# check query string is kept along
|
||||
resp = app.get('/backoffice/i18n/')
|
||||
resp.form['q'] = 'list'
|
||||
resp = resp.form.submit('submit')
|
||||
resp = resp.click('Import')
|
||||
resp.forms[0]['file'] = Upload(
|
||||
'test.ods', output.getvalue(), 'application/vnd.oasis.opendocument.spreadsheet'
|
||||
)
|
||||
resp = resp.form.submit('submit').follow()
|
||||
resp = resp.click('Go to multilinguism')
|
||||
assert resp.request.url == 'http://example.net/backoffice/i18n/?q=list&formdef=&lang=fr'
|
||||
|
||||
# invalid file
|
||||
resp = app.get('/backoffice/i18n/')
|
||||
resp = resp.click('Import')
|
||||
resp.forms[0]['file'] = Upload('test.txt', b'blah')
|
||||
resp = resp.form.submit('submit').follow()
|
||||
resp = app.get('/afterjobs/' + resp.pyquery('.afterjob').attr('id'))
|
||||
assert resp.text == 'failed|Unknown file format'
|
||||
|
||||
|
||||
def test_i18n_pagination(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.fields = []
|
||||
for i in range(90):
|
||||
formdef.fields.append(StringField(id=str(i + 1), label='text field %s' % i))
|
||||
formdef.store()
|
||||
|
||||
# go and scan
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/i18n/', status=302).follow()
|
||||
resp = resp.click('Go to multilinguism page')
|
||||
|
||||
# check page limit
|
||||
assert resp.pyquery('#page-links a').text() == '1 2 3 4 5 10 50 100'
|
||||
resp = resp.click('50')
|
||||
assert resp.pyquery('#page-links a').text() == '1 2 10 20 100'
|
||||
resp = resp.click('20')
|
||||
resp = resp.click('3')
|
||||
assert 'offset=40' in resp.request.url
|
||||
|
||||
|
||||
def test_i18n_mark_as_non_translatabe(pub):
|
||||
create_superuser(pub)
|
||||
workflow = Workflow(name='workflow')
|
||||
workflow.add_status('First Status')
|
||||
workflow.add_status('Second Status')
|
||||
workflow.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
# first time goes to scanning
|
||||
resp = app.get('/backoffice/i18n/', status=302)
|
||||
resp = resp.follow()
|
||||
resp = resp.click('Go to multilinguism page')
|
||||
# second time, the page stays on
|
||||
resp = app.get('/backoffice/i18n/', status=200)
|
||||
|
||||
assert TranslatableMessage.count() == 2 # First Status / Second Status
|
||||
assert resp.pyquery('tr').length == 2
|
||||
|
||||
# check form filter
|
||||
assert resp.form['lang'].value == 'fr'
|
||||
resp.form['q'] = 'First'
|
||||
resp = resp.form.submit()
|
||||
assert resp.pyquery('tr').length == 1
|
||||
|
||||
# mark a message as non translatable
|
||||
resp = resp.click('edit', index=0)
|
||||
resp.form['non_translatable'].checked = True
|
||||
resp = resp.form.submit('submit').follow()
|
||||
msg = TranslatableMessage.select([Equal('string', 'First Status')])[0]
|
||||
assert msg.translatable is False
|
||||
|
||||
resp = app.get('/backoffice/i18n/', status=200)
|
||||
assert resp.pyquery('tr').length == 1
|
||||
assert resp.pyquery('tr td:first-child').text() == 'Second Status'
|
||||
resp.form['non_translatable'].checked = True
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.pyquery('tr').length == 1
|
||||
assert resp.pyquery('tr td:first-child').text() == 'First Status'
|
||||
|
||||
|
||||
def test_i18n_but_no_language(pub):
|
||||
pub.cfg['language'] = {'language': 'en', 'multilinguism': True, 'languages': ['en']}
|
||||
pub.write_cfg()
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/i18n/', status=200)
|
||||
assert 'No languages selected.' in resp.text
|
|
@ -1,455 +0,0 @@
|
|||
import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.workflows import Workflow
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import create_superuser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub(request):
|
||||
pub = create_temporary_pub()
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
pub.set_app_dir(req)
|
||||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.cfg['language'] = {'language': 'en'}
|
||||
pub.write_cfg()
|
||||
return pub
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_studio_home(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/studio/')
|
||||
assert 'Recent errors' in resp.text
|
||||
|
||||
|
||||
def test_listing_paginations(pub):
|
||||
FormDef.wipe()
|
||||
CardDef.wipe()
|
||||
Workflow.wipe()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo'
|
||||
formdef.store()
|
||||
formdef2 = FormDef()
|
||||
formdef2.name = 'foo 2'
|
||||
formdef2.store()
|
||||
carddef = CardDef()
|
||||
carddef.name = 'bar'
|
||||
carddef.store()
|
||||
carddef2 = CardDef()
|
||||
carddef2.name = 'bar 2'
|
||||
carddef2.store()
|
||||
workflow = Workflow()
|
||||
workflow.name = 'blah'
|
||||
workflow.store()
|
||||
workflow2 = Workflow()
|
||||
workflow2.name = 'blah 2'
|
||||
workflow2.store()
|
||||
|
||||
# FormDef errors
|
||||
for i in range(0, 21):
|
||||
error = pub.loggederror_class()
|
||||
error.summary = 'FormDef Workflow Logged Error n°%s' % i
|
||||
error.formdef_class = 'FormDef'
|
||||
error.formdef_id = formdef.id
|
||||
error.workflow_id = workflow.id
|
||||
error.first_occurence_timestamp = datetime.datetime.now()
|
||||
error.store()
|
||||
error = pub.loggederror_class()
|
||||
error.summary = 'FormDef 2 Workflow 2 Logged Error n°%s' % i
|
||||
error.formdef_class = 'FormDef'
|
||||
error.formdef_id = formdef2.id
|
||||
error.workflow_id = workflow2.id
|
||||
error.first_occurence_timestamp = datetime.datetime.now()
|
||||
error.store()
|
||||
|
||||
# CardDef errors
|
||||
for i in range(0, 21):
|
||||
error = pub.loggederror_class()
|
||||
error.summary = 'CardDef Workflow Logged Error n°%s' % i
|
||||
error.formdef_class = 'CardDef'
|
||||
error.formdef_id = carddef.id
|
||||
error.workflow_id = workflow.id
|
||||
error.first_occurence_timestamp = datetime.datetime.now()
|
||||
error.store()
|
||||
error = pub.loggederror_class()
|
||||
error.summary = 'CardDef 2 Workflow 2 Logged Error n°%s' % i
|
||||
error.formdef_class = 'CardDef'
|
||||
error.formdef_id = carddef2.id
|
||||
error.workflow_id = workflow2.id
|
||||
error.first_occurence_timestamp = datetime.datetime.now()
|
||||
error.store()
|
||||
|
||||
# workflow-only errors
|
||||
for i in range(0, 21):
|
||||
error = pub.loggederror_class()
|
||||
error.summary = 'Workflow Logged Error n°%s' % i
|
||||
error.workflow_id = workflow.id
|
||||
error.first_occurence_timestamp = datetime.datetime.now()
|
||||
error.store()
|
||||
error = pub.loggederror_class()
|
||||
error.summary = 'Workflow 2 Logged Error n°%s' % i
|
||||
error.workflow_id = workflow2.id
|
||||
error.first_occurence_timestamp = datetime.datetime.now()
|
||||
error.store()
|
||||
|
||||
# standalone error
|
||||
error = pub.loggederror_class()
|
||||
error.summary = 'Lonely Logged Error'
|
||||
error.exception_class = 'Exception'
|
||||
error.exception_message = 'foo bar'
|
||||
error.first_occurence_timestamp = datetime.datetime.now()
|
||||
error.occurences_count = 17654032
|
||||
error.store()
|
||||
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
|
||||
# all errors
|
||||
|
||||
# default pagination
|
||||
resp = app.get('/backoffice/studio/logged-errors/')
|
||||
assert '1-20/67' in resp.text
|
||||
assert resp.text.count('Lonely Logged Error') == 1
|
||||
assert '<span class="extra-info">- Exception (foo bar)</span>' in resp.text
|
||||
assert '<span class="badge">17,654,032</span>' in resp.text
|
||||
assert resp.text.count('Logged Error n°') == 19
|
||||
resp = resp.click(href=r'\?offset=60')
|
||||
assert '61-67/67' in resp.text
|
||||
assert resp.text.count('Logged Error n°') == 7
|
||||
|
||||
# change pagination
|
||||
resp = app.get('/backoffice/studio/logged-errors/?offset=0&limit=50')
|
||||
assert '1-50/67' in resp.text
|
||||
assert resp.text.count('Lonely Logged Error') == 1
|
||||
assert resp.text.count('Logged Error n°') == 49
|
||||
resp = resp.click('<!--Next Page-->')
|
||||
assert '51-67/67' in resp.text
|
||||
assert resp.text.count('Logged Error n°') == 17
|
||||
|
||||
# formdef errors
|
||||
resp = app.get('/backoffice/forms/%s/' % formdef.id)
|
||||
assert '21 errors' in resp
|
||||
resp = app.get('/backoffice/forms/%s/logged-errors/' % formdef.id)
|
||||
assert '1-20/21' in resp.text
|
||||
assert resp.text.count('FormDef Workflow Logged Error n°') == 20
|
||||
resp = resp.click('<!--Next Page-->')
|
||||
assert '21-21/21' in resp.text
|
||||
assert resp.text.count('FormDef Workflow Logged Error n°') == 1
|
||||
|
||||
# carddef errors
|
||||
resp = app.get('/backoffice/cards/%s/' % carddef.id)
|
||||
assert '21 errors' in resp
|
||||
resp = app.get('/backoffice/cards/%s/logged-errors/' % carddef.id)
|
||||
assert '1-20/21' in resp.text
|
||||
assert resp.text.count('CardDef Workflow Logged Error n°') == 20
|
||||
resp = resp.click('<!--Next Page-->')
|
||||
assert '21-21/21' in resp.text
|
||||
assert resp.text.count('CardDef Workflow Logged Error n°') == 1
|
||||
|
||||
# workflows errors
|
||||
resp = app.get('/backoffice/workflows/%s/' % workflow.id)
|
||||
assert '63 errors' in resp
|
||||
resp = app.get('/backoffice/workflows/%s/logged-errors/' % workflow.id)
|
||||
assert '1-20/63' in resp.text
|
||||
assert resp.text.count('Workflow Logged Error n°') == 20
|
||||
resp = resp.click(href=r'\?offset=60')
|
||||
assert '61-63/63' in resp.text
|
||||
assert resp.text.count('Workflow Logged Error n°') == 3
|
||||
|
||||
|
||||
def test_backoffice_access(pub):
|
||||
FormDef.wipe()
|
||||
CardDef.wipe()
|
||||
Workflow.wipe()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo'
|
||||
formdef.store()
|
||||
carddef = CardDef()
|
||||
carddef.name = 'bar'
|
||||
carddef.store()
|
||||
workflow = Workflow()
|
||||
workflow.name = 'blah'
|
||||
workflow.store()
|
||||
|
||||
# FormDef error
|
||||
error1 = pub.loggederror_class()
|
||||
error1.summary = 'LoggedError'
|
||||
error1.formdef_class = 'FormDef'
|
||||
error1.formdef_id = formdef.id
|
||||
error1.workflow_id = workflow.id
|
||||
error1.first_occurence_timestamp = datetime.datetime.now()
|
||||
error1.store()
|
||||
|
||||
# CardDef error
|
||||
error2 = pub.loggederror_class()
|
||||
error2.summary = 'LoggedError'
|
||||
error2.formdef_class = 'CardDef'
|
||||
error2.formdef_id = carddef.id
|
||||
error2.workflow_id = workflow.id
|
||||
error2.first_occurence_timestamp = datetime.datetime.now()
|
||||
error2.store()
|
||||
|
||||
# workflow-only error
|
||||
error3 = pub.loggederror_class()
|
||||
error3.summary = 'LoggedError'
|
||||
error3.workflow_id = workflow.id
|
||||
error3.first_occurence_timestamp = datetime.datetime.now()
|
||||
error3.store()
|
||||
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
|
||||
# check section link are not displayed if user has no access right
|
||||
|
||||
# formdefs are not accessible to current user
|
||||
pub.cfg['admin-permissions'] = {'forms': ['X']}
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/studio/logged-errors/')
|
||||
assert resp.text.count('LoggedError') == 2
|
||||
assert '<a href="%s/">' % error1.id not in resp.text
|
||||
assert '<a href="%s/">' % error2.id in resp.text
|
||||
assert '<a href="%s/">' % error3.id in resp.text
|
||||
|
||||
# carddefs are not accessible to current user
|
||||
pub.cfg['admin-permissions'] = {'cards': ['X']}
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/studio/logged-errors/')
|
||||
assert resp.text.count('LoggedError') == 2
|
||||
assert '<a href="%s/">' % error1.id in resp.text
|
||||
assert '<a href="%s/">' % error2.id not in resp.text
|
||||
assert '<a href="%s/">' % error3.id in resp.text
|
||||
|
||||
# workflows are not accessible to current user
|
||||
pub.cfg['admin-permissions'] = {'workflows': ['X']}
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/studio/logged-errors/')
|
||||
assert resp.text.count('LoggedError') == 2
|
||||
assert '<a href="%s/">' % error1.id in resp.text
|
||||
assert '<a href="%s/">' % error2.id in resp.text
|
||||
assert '<a href="%s/">' % error3.id not in resp.text
|
||||
|
||||
# mix formdefs & workflows
|
||||
pub.cfg['admin-permissions'] = {'forms': ['X'], 'workflows': ['X']}
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/studio/logged-errors/')
|
||||
assert resp.text.count('LoggedError') == 1
|
||||
assert '<a href="%s/">' % error1.id not in resp.text
|
||||
assert '<a href="%s/">' % error2.id in resp.text
|
||||
assert '<a href="%s/">' % error3.id not in resp.text
|
||||
|
||||
# mix all
|
||||
pub.cfg['admin-permissions'] = {'forms': ['X'], 'cards': ['X'], 'workflows': ['X']}
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/studio/logged-errors/', status=403)
|
||||
|
||||
|
||||
def test_logged_error_404(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
|
||||
# check non-existent id
|
||||
app.get('/backoffice/studio/logged-errors/1', status=404)
|
||||
|
||||
# check invalid (non-integer) id
|
||||
app.get('/backoffice/studio/logged-errors/null', status=404)
|
||||
|
||||
|
||||
def test_logged_error_trace(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
|
||||
logged_error = pub.record_error('Error')
|
||||
resp = app.get(f'/backoffice/studio/logged-errors/{logged_error.id}/')
|
||||
assert 'pub.record_error(\'Error' in resp.pyquery('.stack-trace--code')[0].text
|
||||
assert '\n locals:' in resp.text
|
||||
|
||||
try:
|
||||
raise ZeroDivisionError()
|
||||
except Exception as e:
|
||||
logged_error = pub.record_error('Exception', exception=e)
|
||||
|
||||
resp = app.get(f'/backoffice/studio/logged-errors/{logged_error.id}/')
|
||||
assert 'pub.record_error(\'Exception' in resp.pyquery('.stack-trace--code')[0].text
|
||||
assert '\n locals:' in resp.text
|
||||
|
||||
|
||||
def test_logged_error_cleanup(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
FormDef.wipe()
|
||||
CardDef.wipe()
|
||||
Workflow.wipe()
|
||||
pub.loggederror_class.wipe()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo'
|
||||
formdef.store()
|
||||
carddef = CardDef()
|
||||
carddef.name = 'bar'
|
||||
carddef.store()
|
||||
workflow = Workflow()
|
||||
workflow.name = 'blah'
|
||||
workflow.store()
|
||||
|
||||
# FormDef error
|
||||
error1 = pub.loggederror_class()
|
||||
error1.summary = 'LoggedError'
|
||||
error1.formdef_class = 'FormDef'
|
||||
error1.formdef_id = formdef.id
|
||||
error1.workflow_id = workflow.id
|
||||
error1.first_occurence_timestamp = error1.latest_occurence_timestamp = datetime.datetime.now()
|
||||
error1.store()
|
||||
|
||||
# CardDef error
|
||||
error2 = pub.loggederror_class()
|
||||
error2.summary = 'LoggedError'
|
||||
error2.formdef_class = 'CardDef'
|
||||
error2.formdef_id = carddef.id
|
||||
error2.workflow_id = workflow.id
|
||||
error2.first_occurence_timestamp = error2.latest_occurence_timestamp = datetime.datetime.now()
|
||||
error2.store()
|
||||
|
||||
# workflow-only error
|
||||
error3 = pub.loggederror_class()
|
||||
error3.summary = 'LoggedError'
|
||||
error3.workflow_id = workflow.id
|
||||
error3.first_occurence_timestamp = error3.latest_occurence_timestamp = datetime.datetime.now()
|
||||
error3.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/studio/logged-errors/')
|
||||
resp = resp.click('Cleanup')
|
||||
resp = resp.form.submit('submit')
|
||||
assert pub.loggederror_class().count() == 3 # nothing removed
|
||||
|
||||
# check there's a form error if nothing is checked
|
||||
resp = app.get('/backoffice/studio/logged-errors/')
|
||||
resp = resp.click('Cleanup')
|
||||
resp.form['types$elementformdef'].checked = False
|
||||
resp.form['types$elementcarddef'].checked = False
|
||||
resp.form['types$elementothers'].checked = False
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.pyquery('[data-widget-name="types"].widget-with-error')
|
||||
|
||||
# check cleanup of only formdef errors
|
||||
error1.first_occurence_timestamp = (
|
||||
error1.latest_occurence_timestamp
|
||||
) = datetime.datetime.now() - datetime.timedelta(days=280)
|
||||
error1.store()
|
||||
error2.first_occurence_timestamp = datetime.datetime.now() - datetime.timedelta(days=120)
|
||||
error2.latest_occurence_timestamp = datetime.datetime.now() - datetime.timedelta(days=80)
|
||||
error2.store()
|
||||
error3.first_occurence_timestamp = (
|
||||
error3.latest_occurence_timestamp
|
||||
) = datetime.datetime.now() - datetime.timedelta(days=280)
|
||||
error3.store()
|
||||
resp = app.get('/backoffice/studio/logged-errors/')
|
||||
resp = resp.click('Cleanup')
|
||||
resp.form['types$elementcarddef'].checked = False
|
||||
resp.form['types$elementothers'].checked = False
|
||||
resp = resp.form.submit('submit')
|
||||
assert {x.id for x in pub.loggederror_class().select()} == {error2.id, error3.id}
|
||||
|
||||
# check cleanup latest occurence value (error2 should not be cleaned)
|
||||
resp = app.get('/backoffice/studio/logged-errors/')
|
||||
resp = resp.click('Cleanup')
|
||||
resp.form['latest_occurence'] = (datetime.datetime.now() - datetime.timedelta(days=100)).strftime(
|
||||
'%Y-%m-%d'
|
||||
)
|
||||
resp = resp.form.submit('submit')
|
||||
assert {x.id for x in pub.loggederror_class().select()} == {error2.id}
|
||||
|
||||
# check with a more recent date (error2 should be cleaned this time)
|
||||
resp = app.get('/backoffice/studio/logged-errors/')
|
||||
resp = resp.click('Cleanup')
|
||||
resp.form['latest_occurence'] = (datetime.datetime.now() - datetime.timedelta(days=10)).strftime(
|
||||
'%Y-%m-%d'
|
||||
)
|
||||
resp = resp.form.submit('submit')
|
||||
assert {x.id for x in pub.loggederror_class().select()} == set()
|
||||
|
||||
# make formdefs not accessible to current user
|
||||
pub.cfg['admin-permissions'] = {'forms': ['X']}
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/studio/logged-errors/')
|
||||
resp = resp.click('Cleanup')
|
||||
assert [x.attrib['name'] for x in resp.pyquery('[type="checkbox"]')] == [
|
||||
'types$elementcarddef',
|
||||
'types$elementothers',
|
||||
]
|
||||
|
||||
|
||||
def test_logged_error_cleanup_from_filtered_page(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
FormDef.wipe()
|
||||
CardDef.wipe()
|
||||
Workflow.wipe()
|
||||
pub.loggederror_class.wipe()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo'
|
||||
formdef.store()
|
||||
carddef = CardDef()
|
||||
carddef.name = 'bar'
|
||||
carddef.store()
|
||||
workflow = Workflow()
|
||||
workflow.name = 'blah'
|
||||
workflow.store()
|
||||
|
||||
# FormDef error
|
||||
error1 = pub.loggederror_class()
|
||||
error1.summary = 'LoggedError'
|
||||
error1.formdef_class = 'FormDef'
|
||||
error1.formdef_id = formdef.id
|
||||
error1.first_occurence_timestamp = error1.latest_occurence_timestamp = datetime.datetime.now()
|
||||
error1.store()
|
||||
|
||||
# CardDef error
|
||||
error2 = pub.loggederror_class()
|
||||
error2.summary = 'LoggedError'
|
||||
error2.formdef_class = 'CardDef'
|
||||
error2.formdef_id = carddef.id
|
||||
error2.first_occurence_timestamp = error2.latest_occurence_timestamp = datetime.datetime.now()
|
||||
error2.store()
|
||||
|
||||
# workflow-only error
|
||||
error3 = pub.loggederror_class()
|
||||
error3.summary = 'LoggedError'
|
||||
error3.workflow_id = workflow.id
|
||||
error3.first_occurence_timestamp = error3.latest_occurence_timestamp = datetime.datetime.now()
|
||||
error3.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get(formdef.get_admin_url() + 'logged-errors/')
|
||||
resp = resp.click('Cleanup')
|
||||
resp.form['latest_occurence'] = (datetime.datetime.now() + datetime.timedelta(days=1)).strftime(
|
||||
'%Y-%m-%d'
|
||||
)
|
||||
resp = resp.form.submit('submit')
|
||||
assert not pub.loggederror_class.has_key(error1.id)
|
||||
assert pub.loggederror_class.has_key(error2.id)
|
||||
assert pub.loggederror_class.has_key(error3.id)
|
||||
|
||||
resp = app.get(workflow.get_admin_url() + 'logged-errors/')
|
||||
resp = resp.click('Cleanup')
|
||||
resp.form['latest_occurence'] = (datetime.datetime.now() + datetime.timedelta(days=1)).strftime(
|
||||
'%Y-%m-%d'
|
||||
)
|
||||
resp = resp.form.submit('submit')
|
||||
assert not pub.loggederror_class.has_key(error1.id)
|
||||
assert pub.loggederror_class.has_key(error2.id)
|
||||
assert not pub.loggederror_class.has_key(error3.id)
|
|
@ -1,124 +0,0 @@
|
|||
import pytest
|
||||
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import create_superuser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub(request):
|
||||
pub = create_temporary_pub()
|
||||
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
pub.set_app_dir(req)
|
||||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.cfg['language'] = {'language': 'en'}
|
||||
pub.write_cfg()
|
||||
|
||||
return pub
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_roles(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
app.get('/backoffice/roles/')
|
||||
|
||||
|
||||
def test_roles_new(pub):
|
||||
create_superuser(pub)
|
||||
pub.role_class.wipe()
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/roles/')
|
||||
resp = resp.click('New Role')
|
||||
resp.forms[0]['name'] = 'a new role'
|
||||
resp.forms[0]['details'] = 'bla bla bla'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/roles/'
|
||||
resp = resp.follow()
|
||||
assert 'a new role' in resp.text
|
||||
resp = resp.click('a new role')
|
||||
assert '<h2>a new role' in resp.text
|
||||
|
||||
assert pub.role_class.get(1).name == 'a new role'
|
||||
assert pub.role_class.get(1).details == 'bla bla bla'
|
||||
|
||||
|
||||
def test_roles_edit(pub):
|
||||
create_superuser(pub)
|
||||
pub.role_class.wipe()
|
||||
role = pub.role_class(name='foobar')
|
||||
role.allows_backoffice_access = True
|
||||
role.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/roles/1/')
|
||||
assert 'Holders of this role are granted access to the backoffice' in resp.text
|
||||
|
||||
resp = resp.click(href='edit')
|
||||
assert resp.forms[0]['name'].value == 'foobar'
|
||||
resp.forms[0]['name'] = 'baz'
|
||||
resp.forms[0]['details'] = 'bla bla bla'
|
||||
resp.forms[0]['emails_to_members'].checked = True
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/roles/1/'
|
||||
resp = resp.follow()
|
||||
assert '<h2>baz' in resp.text
|
||||
assert 'Holders of this role will receive all emails adressed to the role.' in resp.text
|
||||
|
||||
assert pub.role_class.get(1).details == 'bla bla bla'
|
||||
assert pub.role_class.get(1).emails_to_members is True
|
||||
|
||||
|
||||
def test_roles_matching_formdefs(pub):
|
||||
create_superuser(pub)
|
||||
pub.role_class.wipe()
|
||||
role = pub.role_class(name='foo')
|
||||
role.store()
|
||||
|
||||
FormDef.wipe()
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/roles/1/')
|
||||
assert 'form bar' not in resp.text
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form bar'
|
||||
formdef.roles = [role.id]
|
||||
formdef.fields = []
|
||||
formdef.store()
|
||||
|
||||
resp = app.get('/backoffice/roles/1/')
|
||||
assert 'form bar' in resp.text
|
||||
assert 'form baz' not in resp.text
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form baz'
|
||||
formdef.fields = []
|
||||
formdef.workflow_roles = {'_receiver': role.id}
|
||||
formdef.store()
|
||||
|
||||
resp = app.get('/backoffice/roles/1/')
|
||||
assert 'form baz' in resp.text
|
||||
assert 'form bar' not in resp.text
|
||||
|
||||
|
||||
def test_roles_delete(pub):
|
||||
create_superuser(pub)
|
||||
pub.role_class.wipe()
|
||||
role = pub.role_class(name='foobar')
|
||||
role.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/roles/1/')
|
||||
|
||||
resp = resp.click(href='delete')
|
||||
resp = resp.forms[0].submit()
|
||||
assert resp.location == 'http://example.net/backoffice/roles/'
|
||||
resp = resp.follow()
|
||||
assert pub.role_class.count() == 0
|
|
@ -1,381 +0,0 @@
|
|||
import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
import pytest
|
||||
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.comment_templates import CommentTemplate
|
||||
from wcs.data_sources import NamedDataSource
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.mail_templates import MailTemplate
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.sql_criterias import Equal
|
||||
from wcs.workflows import Workflow
|
||||
from wcs.wscalls import NamedWsCall
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import create_superuser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub():
|
||||
pub = create_temporary_pub()
|
||||
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
pub.set_app_dir(req)
|
||||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.cfg['language'] = {'language': 'en'}
|
||||
pub.write_cfg()
|
||||
|
||||
return pub
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_studio_home(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/')
|
||||
assert 'studio' in resp.text
|
||||
resp = app.get('/backoffice/studio/')
|
||||
assert '../forms/' in resp.text
|
||||
assert '../cards/' in resp.text
|
||||
assert '../workflows/' in resp.text
|
||||
assert '../forms/data-sources/' in resp.text
|
||||
assert '../workflows/data-sources/' not in resp.text
|
||||
assert '../settings/data-sources/' not in resp.text
|
||||
assert '../forms/blocks/' in resp.text
|
||||
assert '../workflows/mail-templates/' in resp.text
|
||||
assert '../workflows/comment-templates/' in resp.text
|
||||
assert '../settings/wscalls/' in resp.text
|
||||
assert 'Recent errors' in resp.text
|
||||
|
||||
pub.cfg['admin-permissions'] = {}
|
||||
for part in ('forms', 'cards', 'workflows'):
|
||||
# check section link are not displayed if user has no access right
|
||||
pub.cfg['admin-permissions'].update({part: ['x']}) # block access
|
||||
pub.write_cfg()
|
||||
if part != 'workflows':
|
||||
resp = app.get('/backoffice/studio/')
|
||||
assert '../%s/' % part not in resp.text
|
||||
assert '../forms/data-sources/' not in resp.text
|
||||
assert '../workflows/data-sources/' in resp.text
|
||||
assert '../settings/data-sources/' not in resp.text
|
||||
else:
|
||||
resp = app.get('/backoffice/studio/', status=403) # totally closed
|
||||
|
||||
resp = app.get('/backoffice/')
|
||||
assert 'backoffice/studio' not in resp.text
|
||||
|
||||
# access to cards only (and settings)
|
||||
pub.cfg['admin-permissions'] = {}
|
||||
pub.cfg['admin-permissions'].update({'forms': ['x'], 'workflows': ['x']})
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/studio/')
|
||||
assert '../forms/' not in resp.text
|
||||
assert '../cards/' in resp.text
|
||||
assert '../workflows/' not in resp.text
|
||||
assert '../settings/data-sources/' in resp.text
|
||||
assert '../settings/wscalls/' in resp.text
|
||||
|
||||
# no access to settings
|
||||
pub.cfg['admin-permissions'].update({'settings': ['x']})
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/studio/')
|
||||
assert '../forms/' not in resp.text
|
||||
assert '../cards/' in resp.text
|
||||
assert '../workflows/' not in resp.text
|
||||
assert '../settings/' not in resp.text
|
||||
|
||||
|
||||
def test_studio_home_recent_errors(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/studio/')
|
||||
assert 'No errors' in resp.text
|
||||
|
||||
def new_error():
|
||||
error = pub.loggederror_class()
|
||||
error.summary = 'Lonely Logged Error'
|
||||
error.exception_class = 'Exception'
|
||||
error.exception_message = 'foo bar'
|
||||
error.first_occurence_timestamp = datetime.datetime.now()
|
||||
error.occurences_count = 17654032
|
||||
error.store()
|
||||
return error
|
||||
|
||||
errors = [new_error()]
|
||||
resp = app.get('/backoffice/studio/')
|
||||
assert 'No errors' not in resp.text
|
||||
assert resp.text.count('logged-errors/') == 2
|
||||
assert 'logged-errors/%s/' % errors[0].id in resp
|
||||
|
||||
for i in range(5):
|
||||
errors.append(new_error())
|
||||
resp = app.get('/backoffice/studio/')
|
||||
assert resp.text.count('logged-errors/') == 6
|
||||
# five recent errors displayed
|
||||
assert 'logged-errors/%s/' % errors[0].id not in resp
|
||||
assert 'logged-errors/%s/' % errors[1].id in resp
|
||||
assert 'logged-errors/%s/' % errors[2].id in resp
|
||||
assert 'logged-errors/%s/' % errors[3].id in resp
|
||||
assert 'logged-errors/%s/' % errors[4].id in resp
|
||||
assert 'logged-errors/%s/' % errors[5].id in resp
|
||||
|
||||
|
||||
def test_studio_home_recent_changes(pub):
|
||||
create_superuser(pub)
|
||||
user = create_superuser(pub)
|
||||
other_user = pub.user_class(name='other')
|
||||
other_user.store()
|
||||
|
||||
pub.snapshot_class.wipe()
|
||||
BlockDef.wipe()
|
||||
CardDef.wipe()
|
||||
NamedDataSource.wipe()
|
||||
FormDef.wipe()
|
||||
MailTemplate.wipe()
|
||||
CommentTemplate.wipe()
|
||||
Workflow.wipe()
|
||||
NamedWsCall.wipe()
|
||||
|
||||
objects = defaultdict(list)
|
||||
for i in range(6):
|
||||
for klass in [
|
||||
BlockDef,
|
||||
CardDef,
|
||||
NamedDataSource,
|
||||
FormDef,
|
||||
MailTemplate,
|
||||
CommentTemplate,
|
||||
Workflow,
|
||||
NamedWsCall,
|
||||
]:
|
||||
obj = klass()
|
||||
obj.name = 'foo %s' % i
|
||||
obj.store()
|
||||
objects[klass.xml_root_node].append(obj)
|
||||
for klass in [
|
||||
BlockDef,
|
||||
CardDef,
|
||||
NamedDataSource,
|
||||
FormDef,
|
||||
MailTemplate,
|
||||
CommentTemplate,
|
||||
Workflow,
|
||||
NamedWsCall,
|
||||
]:
|
||||
assert pub.snapshot_class.count(clause=[Equal('object_type', klass.xml_root_node)]) == 6
|
||||
# 2 snapshots for this one, but will be displayed only once
|
||||
objects[klass.xml_root_node][-1].name += ' bar'
|
||||
objects[klass.xml_root_node][-1].store()
|
||||
assert pub.snapshot_class.count(clause=[Equal('object_type', klass.xml_root_node)]) == 7
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/studio/')
|
||||
assert len(resp.pyquery.find('ul.recent-changes li')) == 0
|
||||
|
||||
for snapshot in pub.snapshot_class.select():
|
||||
snapshot.user_id = other_user.id
|
||||
snapshot.store()
|
||||
resp = app.get('/backoffice/studio/')
|
||||
assert len(resp.pyquery.find('ul.recent-changes li')) == 0
|
||||
|
||||
for snapshot in pub.snapshot_class.select():
|
||||
snapshot.user_id = user.id
|
||||
snapshot.store()
|
||||
resp = app.get('/backoffice/studio/')
|
||||
assert len(resp.pyquery.find('ul.recent-changes li')) == 5
|
||||
|
||||
# too old
|
||||
for i in range(5):
|
||||
assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][i].id not in resp
|
||||
assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][i].id not in resp
|
||||
assert (
|
||||
'backoffice/settings/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
|
||||
)
|
||||
assert 'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
|
||||
assert (
|
||||
'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
|
||||
)
|
||||
assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][i].id not in resp
|
||||
assert (
|
||||
'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp
|
||||
)
|
||||
assert (
|
||||
'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][i].id
|
||||
not in resp
|
||||
)
|
||||
assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp
|
||||
assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][i].id not in resp
|
||||
|
||||
# too old
|
||||
assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][5].id not in resp
|
||||
assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id not in resp
|
||||
assert 'backoffice/settings/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id not in resp
|
||||
# only 5 elements
|
||||
assert (
|
||||
'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id not in resp
|
||||
) # not this url
|
||||
assert (
|
||||
'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id
|
||||
not in resp # not this url
|
||||
)
|
||||
assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][5].id in resp
|
||||
assert 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][5].id in resp
|
||||
assert 'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][5].id in resp
|
||||
assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][5].id in resp
|
||||
assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][5].id in resp
|
||||
|
||||
pub.cfg['admin-permissions'] = {}
|
||||
pub.cfg['admin-permissions'].update({'settings': ['x']})
|
||||
pub.write_cfg()
|
||||
|
||||
resp = app.get('/backoffice/studio/')
|
||||
# no access to settings
|
||||
for i in range(6):
|
||||
assert (
|
||||
'backoffice/settings/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
|
||||
)
|
||||
assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][i].id not in resp
|
||||
# too old
|
||||
for i in range(5):
|
||||
assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][i].id not in resp
|
||||
assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][i].id not in resp
|
||||
assert 'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
|
||||
assert (
|
||||
'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
|
||||
)
|
||||
assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][i].id not in resp
|
||||
assert (
|
||||
'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp
|
||||
)
|
||||
assert (
|
||||
'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][i].id
|
||||
not in resp
|
||||
)
|
||||
assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp
|
||||
# too old
|
||||
assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][5].id not in resp
|
||||
assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id not in resp
|
||||
# only 5 elements
|
||||
assert 'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id in resp
|
||||
assert (
|
||||
'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id
|
||||
not in resp # not this url
|
||||
)
|
||||
assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][5].id in resp
|
||||
assert 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][5].id in resp
|
||||
assert 'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][5].id in resp
|
||||
assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][5].id in resp
|
||||
|
||||
pub.cfg['admin-permissions'] = {}
|
||||
pub.cfg['admin-permissions'].update({'settings': ['x'], 'forms': ['x']})
|
||||
pub.write_cfg()
|
||||
|
||||
resp = app.get('/backoffice/studio/')
|
||||
# no access to settings or forms
|
||||
for i in range(6):
|
||||
assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][i].id not in resp
|
||||
assert (
|
||||
'backoffice/settings/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
|
||||
)
|
||||
assert 'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
|
||||
assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][i].id not in resp
|
||||
assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][i].id not in resp
|
||||
# too old
|
||||
for i in range(5):
|
||||
assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][i].id not in resp
|
||||
assert (
|
||||
'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
|
||||
)
|
||||
assert (
|
||||
'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp
|
||||
)
|
||||
assert (
|
||||
'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][i].id
|
||||
not in resp
|
||||
)
|
||||
assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp
|
||||
# only 5 elements
|
||||
assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id in resp
|
||||
assert 'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][5].id in resp
|
||||
assert 'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][5].id in resp
|
||||
assert 'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][5].id in resp
|
||||
assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][5].id in resp
|
||||
|
||||
pub.cfg['admin-permissions'] = {}
|
||||
pub.cfg['admin-permissions'].update({'settings': ['x'], 'forms': ['x'], 'workflows': ['x']})
|
||||
pub.write_cfg()
|
||||
|
||||
resp = app.get('/backoffice/studio/')
|
||||
# no access to settings, forms or workflows
|
||||
for i in range(6):
|
||||
assert 'backoffice/forms/blocks/%s/' % objects[BlockDef.xml_root_node][i].id not in resp
|
||||
assert (
|
||||
'backoffice/settings/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
|
||||
)
|
||||
assert 'backoffice/forms/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
|
||||
assert 'backoffice/forms/%s/' % objects[FormDef.xml_root_node][i].id not in resp
|
||||
assert 'backoffice/settings/wscalls/%s/' % objects[NamedWsCall.xml_root_node][i].id not in resp
|
||||
assert (
|
||||
'backoffice/workflows/data-sources/%s/' % objects[NamedDataSource.xml_root_node][i].id not in resp
|
||||
)
|
||||
assert (
|
||||
'backoffice/workflows/mail-templates/%s/' % objects[MailTemplate.xml_root_node][i].id not in resp
|
||||
)
|
||||
assert (
|
||||
'backoffice/workflows/comment-templates/%s/' % objects[CommentTemplate.xml_root_node][i].id
|
||||
not in resp
|
||||
)
|
||||
assert 'backoffice/workflows/%s/' % objects[Workflow.xml_root_node][i].id not in resp
|
||||
# too old
|
||||
assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][0].id not in resp
|
||||
# only 5 elements
|
||||
for i in range(1, 6):
|
||||
assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][i].id in resp
|
||||
|
||||
objects[CardDef.xml_root_node][5].remove_self()
|
||||
resp = app.get('/backoffice/studio/')
|
||||
# too old
|
||||
assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][0].id not in resp
|
||||
# only 4 elements, one was deleted
|
||||
for i in range(1, 5):
|
||||
assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][i].id in resp
|
||||
# deleted
|
||||
assert 'backoffice/cards/%s/' % objects[CardDef.xml_root_node][5].id not in resp
|
||||
|
||||
# all changes page: admin user can see all changes (depending on permissions)
|
||||
resp = resp.click(href='all-changes/')
|
||||
assert '(1-6/6)' in resp
|
||||
# he can also see changes from other users
|
||||
for snapshot in pub.snapshot_class.select():
|
||||
snapshot.user_id = other_user.id
|
||||
snapshot.store()
|
||||
|
||||
pub.cfg['admin-permissions'] = {}
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/studio/all-changes/')
|
||||
assert '(1-20/48)' in resp
|
||||
resp = resp.click('<!--Next Page-->')
|
||||
assert '21-40/48' in resp.text
|
||||
resp = resp.click('<!--Next Page-->')
|
||||
assert '41-48/48' in resp.text
|
||||
|
||||
user.is_admin = False
|
||||
user.store()
|
||||
app.get('/backoffice/studio/all-changes/', status=403)
|
||||
|
||||
|
||||
def test_studio_workflows(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/workflows/')
|
||||
resp = resp.click(r'Default \(cards\)')
|
||||
assert 'status/recorded/' in resp.text
|
||||
assert 'status/deleted/' in resp.text
|
||||
assert 'This is the default workflow,' in resp.text
|
|
@ -1,347 +0,0 @@
|
|||
import pytest
|
||||
|
||||
from wcs import fields
|
||||
from wcs.admin.settings import UserFieldsFormDef
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.qommon.ident.password_accounts import PasswordAccount
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import create_role, create_superuser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub(request):
|
||||
pub = create_temporary_pub()
|
||||
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
pub.set_app_dir(req)
|
||||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.cfg['language'] = {'language': 'en'}
|
||||
pub.write_cfg()
|
||||
|
||||
return pub
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_users(pub):
|
||||
create_superuser(pub)
|
||||
app = login(get_app(pub))
|
||||
app.get('/backoffice/users/')
|
||||
|
||||
|
||||
def test_users_new(pub):
|
||||
pub.user_class.wipe()
|
||||
create_superuser(pub)
|
||||
user_count = pub.user_class.count()
|
||||
account_count = PasswordAccount.count()
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/users/')
|
||||
resp = resp.click('New User')
|
||||
resp.forms[0]['name'] = 'a new user'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/users/'
|
||||
resp = resp.follow()
|
||||
assert 'a new user' in resp.text
|
||||
resp = resp.click('a new user')
|
||||
assert 'User - a new user' in resp.text
|
||||
assert pub.user_class.count() == user_count + 1
|
||||
assert PasswordAccount.count() == account_count
|
||||
|
||||
|
||||
def test_users_new_with_account(pub):
|
||||
pub.user_class.wipe()
|
||||
PasswordAccount.wipe()
|
||||
user = create_superuser(pub)
|
||||
user_count = pub.user_class.count()
|
||||
account_count = PasswordAccount.count()
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/users/')
|
||||
resp = resp.click('New User')
|
||||
resp.forms[0]['name'] = 'a second user'
|
||||
resp.forms[0]['method_password$username'] = 'second-user'
|
||||
resp.forms[0]['method_password$password'] = 'foobar'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/users/'
|
||||
resp = resp.follow()
|
||||
assert 'a second user' in resp.text
|
||||
assert 'user-inactive' not in resp.text
|
||||
resp = resp.click('a second user')
|
||||
assert 'User - a second user' in resp.text
|
||||
assert pub.user_class.count() == user_count + 1
|
||||
assert PasswordAccount.count() == account_count + 1
|
||||
|
||||
user = pub.user_class.get(int(user.id) + 1)
|
||||
user.is_active = False
|
||||
user.store()
|
||||
resp = app.get('/backoffice/users/')
|
||||
assert 'user-inactive' in resp.text
|
||||
|
||||
|
||||
def test_users_edit(pub):
|
||||
pub.user_class.wipe()
|
||||
create_superuser(pub)
|
||||
user = pub.user_class(name='foo bar')
|
||||
user.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/users/%s/' % user.id)
|
||||
assert 'This user is not active.' not in resp.text
|
||||
resp = resp.click(href='edit')
|
||||
resp.forms[0]['is_admin'].checked = True
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/users/%s/' % user.id
|
||||
resp = resp.follow()
|
||||
|
||||
user.is_active = False
|
||||
user.store()
|
||||
resp = app.get('/backoffice/users/%s/' % user.id)
|
||||
assert 'This user is not active.' in resp.text
|
||||
|
||||
|
||||
def test_users_edit_new_account(pub):
|
||||
pub.user_class.wipe()
|
||||
PasswordAccount.wipe()
|
||||
create_superuser(pub)
|
||||
user = pub.user_class(name='foo bar')
|
||||
user.store()
|
||||
account_count = PasswordAccount.count()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/users/%s/' % user.id)
|
||||
resp = resp.click(href='edit')
|
||||
resp.forms[0]['is_admin'].checked = True
|
||||
resp.forms[0]['method_password$username'] = 'foo'
|
||||
resp.forms[0]['method_password$password'] = 'bar'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/users/%s/' % user.id
|
||||
resp = resp.follow()
|
||||
|
||||
assert PasswordAccount.count() == account_count + 1
|
||||
|
||||
|
||||
def test_users_edit_edit_account(pub):
|
||||
pub.user_class.wipe()
|
||||
PasswordAccount.wipe()
|
||||
create_superuser(pub)
|
||||
user = pub.user_class(name='foo bar')
|
||||
user.store()
|
||||
account = PasswordAccount(id='test')
|
||||
account.user_id = user.id
|
||||
account.store()
|
||||
assert PasswordAccount.has_key('test')
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/users/%s/' % user.id)
|
||||
resp = resp.click(href='edit')
|
||||
resp.forms[0]['is_admin'].checked = True
|
||||
resp.forms[0]['method_password$username'] = 'foo' # change username
|
||||
resp.forms[0]['method_password$password'] = 'bar'
|
||||
resp = resp.forms[0].submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/users/%s/' % user.id
|
||||
resp = resp.follow()
|
||||
|
||||
# makes sure the old account has been removed
|
||||
assert not PasswordAccount.has_key('test')
|
||||
assert PasswordAccount.has_key('foo')
|
||||
assert PasswordAccount.get('foo').user_id == user.id
|
||||
|
||||
|
||||
def test_users_edit_with_managing_idp(pub):
|
||||
create_role(pub)
|
||||
pub.user_class.wipe()
|
||||
pub.cfg['sp'] = {'idp-manage-user-attributes': True}
|
||||
pub.write_cfg()
|
||||
PasswordAccount.wipe()
|
||||
create_superuser(pub)
|
||||
user = pub.user_class(name='foo bar')
|
||||
user.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/users/%s/' % user.id)
|
||||
assert '>Manage Roles<' in resp.text
|
||||
resp = resp.click(href='edit')
|
||||
assert 'email' not in resp.form.fields
|
||||
assert 'roles$add_element' in resp.form.fields
|
||||
|
||||
pub.cfg['sp'] = {'idp-manage-roles': True}
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/users/%s/' % user.id)
|
||||
assert '>Edit<' in resp.text
|
||||
resp = resp.click(href='edit')
|
||||
assert 'email' in resp.form.fields
|
||||
assert 'roles$add_element' not in resp.form.fields
|
||||
|
||||
pub.cfg['sp'] = {'idp-manage-roles': True, 'idp-manage-user-attributes': True}
|
||||
pub.write_cfg()
|
||||
resp = app.get('/backoffice/users/%s/' % user.id)
|
||||
assert '/edit' not in resp.text
|
||||
|
||||
|
||||
def test_users_delete(pub):
|
||||
pub.user_class.wipe()
|
||||
PasswordAccount.wipe()
|
||||
create_superuser(pub)
|
||||
user = pub.user_class(name='foo bar')
|
||||
user.store()
|
||||
account = PasswordAccount(id='test')
|
||||
account.user_id = user.id
|
||||
account.store()
|
||||
|
||||
user_count = pub.user_class.count()
|
||||
account_count = PasswordAccount.count()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/users/%s/' % user.id)
|
||||
|
||||
resp = resp.click(href='delete')
|
||||
resp = resp.forms[0].submit()
|
||||
assert resp.location == 'http://example.net/backoffice/users/'
|
||||
resp = resp.follow()
|
||||
|
||||
assert pub.user_class.count() == user_count - 1
|
||||
assert PasswordAccount.count() == account_count - 1
|
||||
|
||||
|
||||
def test_users_view_deleted(pub):
|
||||
pub.user_class.wipe()
|
||||
PasswordAccount.wipe()
|
||||
create_superuser(pub)
|
||||
user = pub.user_class(name='foo bar')
|
||||
user.store()
|
||||
account = PasswordAccount(id='test')
|
||||
account.user_id = user.id
|
||||
account.store()
|
||||
|
||||
user.set_deleted()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/users/%s/' % user.id)
|
||||
assert 'Marked as deleted on' in resp
|
||||
|
||||
|
||||
def test_users_pagination(pub):
|
||||
pub.user_class.wipe()
|
||||
PasswordAccount.wipe()
|
||||
create_superuser(pub)
|
||||
for i in range(50):
|
||||
user = pub.user_class(name='foo bar %s' % (i + 1))
|
||||
user.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/users/')
|
||||
assert 'foo bar 10' in resp.text
|
||||
assert 'foo bar 30' not in resp.text
|
||||
|
||||
resp = resp.click('Next Page')
|
||||
assert 'foo bar 10' not in resp.text
|
||||
assert 'foo bar 30' in resp.text
|
||||
|
||||
resp = resp.click('Previous Page')
|
||||
assert 'foo bar 10' in resp.text
|
||||
assert 'foo bar 30' not in resp.text
|
||||
|
||||
resp = resp.click('Next Page')
|
||||
resp = resp.click('Next Page')
|
||||
assert 'foo bar 50' in resp.text
|
||||
|
||||
|
||||
def test_users_filter(pub):
|
||||
pub.user_class.wipe()
|
||||
PasswordAccount.wipe()
|
||||
create_superuser(pub)
|
||||
role = create_role(pub)
|
||||
for i in range(50):
|
||||
user = pub.user_class(name='foo bar %s' % (i + 1))
|
||||
user.store()
|
||||
|
||||
for i in range(5):
|
||||
user = pub.user_class(name='baz bar %s' % (i + 1))
|
||||
user.roles = [role.id]
|
||||
user.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/users/')
|
||||
assert 'admin' in resp.text # superuser
|
||||
assert 'foo bar 10' in resp.text # simple user
|
||||
|
||||
# uncheck 'None'; unfortunately this doesn't work with webtest 1.3
|
||||
# resp.forms[0].fields['role'][-1].checked = False
|
||||
# resp = resp.forms[0].submit()
|
||||
# therefore we fall back on using the URL
|
||||
resp = app.get('/backoffice/users/?offset=0&limit=100&q=&filter=true&role=admin')
|
||||
assert '>Number of filtered users: 1<' in resp.text
|
||||
assert 'user-is-admin' in resp.text # superuser
|
||||
assert 'foo bar 1' not in resp.text # simple user
|
||||
assert 'baz bar 1' not in resp.text # user with role
|
||||
|
||||
resp = app.get('/backoffice/users/?offset=0&limit=100&q=&filter=true&role=1')
|
||||
assert '>Number of filtered users: 5<' in resp.text
|
||||
assert 'user-is-admin' not in resp.text # superuser
|
||||
assert 'foo bar 10' not in resp.text # simple user
|
||||
assert 'baz bar 1' in resp.text # user with role
|
||||
|
||||
|
||||
def test_users_search(pub):
|
||||
pub.user_class.wipe()
|
||||
PasswordAccount.wipe()
|
||||
create_superuser(pub)
|
||||
for i in range(20):
|
||||
user = pub.user_class(name='foo %s' % (i + 1))
|
||||
user.store()
|
||||
for i in range(10):
|
||||
user = pub.user_class(name='bar %s' % (i + 1))
|
||||
user.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/users/')
|
||||
assert 'foo 10' in resp.text
|
||||
|
||||
resp.forms[0]['q'] = 'bar'
|
||||
resp = resp.forms[0].submit()
|
||||
assert 'foo 10' not in resp.text
|
||||
assert 'bar 10' in resp.text
|
||||
assert 'Number of filtered users: 10' in resp.text
|
||||
|
||||
|
||||
def test_users_new_with_custom_formdef(pub):
|
||||
pub.user_class.wipe()
|
||||
formdef = UserFieldsFormDef(pub)
|
||||
formdef.fields.append(fields.StringField(id='3', label='test'))
|
||||
formdef.fields.append(fields.CommentField(id='4', label='test'))
|
||||
formdef.fields.append(fields.FileField(id='5', label='test', required=False))
|
||||
formdef.store()
|
||||
|
||||
create_superuser(pub)
|
||||
user_count = pub.user_class.count()
|
||||
account_count = PasswordAccount.count()
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/users/')
|
||||
resp = resp.click('New User')
|
||||
resp.form['name'] = 'a new user'
|
||||
resp.form['f3'] = 'TEST'
|
||||
resp = resp.form.submit('submit')
|
||||
assert resp.location == 'http://example.net/backoffice/users/'
|
||||
resp = resp.follow()
|
||||
assert 'a new user' in resp.text
|
||||
resp = resp.click('a new user')
|
||||
assert 'User - a new user' in resp.text
|
||||
assert 'TEST' in resp.text
|
||||
assert pub.user_class.count() == user_count + 1
|
||||
assert PasswordAccount.count() == account_count
|
||||
|
||||
|
||||
def test_users_display_roles(pub):
|
||||
pub.user_class.wipe()
|
||||
|
||||
user = create_superuser(pub)
|
||||
role = create_role(pub)
|
||||
user.roles = [role.id, 'XXX']
|
||||
user.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/users/%s/' % user.id)
|
||||
assert role.name in resp.text
|
||||
assert 'Unknown role (XXX)' in resp.text
|
|
@ -1,906 +0,0 @@
|
|||
import datetime
|
||||
|
||||
import pytest
|
||||
from django.utils.html import escape
|
||||
from django.utils.timezone import make_aware
|
||||
|
||||
from wcs import workflow_tests
|
||||
from wcs.formdef import FormDef, fields
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs.testdef import TestDef, WebserviceResponse
|
||||
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef, WorkflowCriticalityLevel
|
||||
|
||||
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
|
||||
from .test_all import create_superuser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub():
|
||||
pub = create_temporary_pub()
|
||||
|
||||
req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
|
||||
pub.set_app_dir(req)
|
||||
pub.cfg['identification'] = {'methods': ['password']}
|
||||
pub.write_cfg()
|
||||
|
||||
pub.user_class.wipe()
|
||||
FormDef.wipe()
|
||||
TestDef.wipe()
|
||||
WebserviceResponse.wipe()
|
||||
return pub
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
clean_temporary_pub()
|
||||
|
||||
|
||||
def test_workflow_tests_options(pub):
|
||||
create_superuser(pub)
|
||||
user = pub.user_class(name='test user')
|
||||
user.email = 'test@example.com'
|
||||
user.test_uuid = '42'
|
||||
user.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.name = 'First test'
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
resp = resp.click('Options')
|
||||
|
||||
resp.form['agent'] = user.test_uuid
|
||||
resp = resp.form.submit('submit').follow()
|
||||
|
||||
testdef = TestDef.get(testdef.id)
|
||||
assert testdef.agent_id == user.test_uuid
|
||||
|
||||
|
||||
def test_workflow_tests_edit_actions(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.name = 'First test'
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/' % testdef.id)
|
||||
resp = resp.click('Workflow tests')
|
||||
|
||||
assert 'There are no workflow test actions yet.' in resp.text
|
||||
assert len(resp.pyquery('.biglist li')) == 0
|
||||
|
||||
option_labels = [x[2] for x in resp.form['type'].options]
|
||||
assert (
|
||||
option_labels.index('Assert email is sent')
|
||||
< option_labels.index('Assert form status')
|
||||
< option_labels.index('—')
|
||||
< option_labels.index('Move forward in time')
|
||||
< option_labels.index('Simulate click on action button')
|
||||
)
|
||||
|
||||
# add workflow test action through sidebar form
|
||||
resp.form['type'] = 'button-click'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'There are no workflow test actions yet.' not in resp.text
|
||||
assert len(resp.pyquery('.biglist li')) == 1
|
||||
assert resp.pyquery('.biglist li .label').text() == 'Simulate click on action button'
|
||||
assert 'not configured' in resp.text
|
||||
|
||||
resp = resp.click('Edit')
|
||||
resp.form['button_name'] = 'Accept'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'not configured' not in resp.text
|
||||
assert [x.text for x in resp.pyquery('ul li.workflow-test-action span.type')] == [
|
||||
'Click on "Accept" by backoffice user',
|
||||
]
|
||||
|
||||
resp = resp.click('Duplicate').follow()
|
||||
assert [x.text for x in resp.pyquery('ul li.workflow-test-action span.type')] == [
|
||||
'Click on "Accept" by backoffice user',
|
||||
'Click on "Accept" by backoffice user',
|
||||
]
|
||||
|
||||
resp = resp.click('Edit', index=0)
|
||||
resp.form['button_name'] = 'Reject'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert [x.text for x in resp.pyquery('ul li.workflow-test-action span.type')] == [
|
||||
'Click on "Reject" by backoffice user',
|
||||
'Click on "Accept" by backoffice user',
|
||||
]
|
||||
|
||||
resp = resp.click('Duplicate', index=0).follow()
|
||||
assert [x.text for x in resp.pyquery('ul li.workflow-test-action span.type')] == [
|
||||
'Click on "Reject" by backoffice user',
|
||||
'Click on "Reject" by backoffice user',
|
||||
'Click on "Accept" by backoffice user',
|
||||
]
|
||||
|
||||
resp = resp.click('Delete', index=0)
|
||||
resp = resp.form.submit().follow()
|
||||
assert [x.text for x in resp.pyquery('ul li.workflow-test-action span.type')] == [
|
||||
'Click on "Reject" by backoffice user',
|
||||
'Click on "Accept" by backoffice user',
|
||||
]
|
||||
|
||||
# simulate invalid action
|
||||
testdef = TestDef.get(testdef.id)
|
||||
testdef.workflow_tests.actions[0].key = 'xxx'
|
||||
testdef.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert [x.text for x in resp.pyquery('ul li.workflow-test-action span.type')] == [
|
||||
'Click on "Accept" by backoffice user',
|
||||
]
|
||||
|
||||
|
||||
def test_workflow_tests_action_button_click(pub):
|
||||
create_superuser(pub)
|
||||
user = pub.user_class(name='test user')
|
||||
user.test_uuid = '42'
|
||||
user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(id='1', button_name='Button 4'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert escape('Click on "Button 4" by backoffice user') in resp.text
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert 'Workflow has no action that displays a button.' in resp.text
|
||||
|
||||
jump = new_status.add_action('choice')
|
||||
jump.label = 'Button 1'
|
||||
jump.status = new_status.id
|
||||
|
||||
jump = new_status.add_action('choice')
|
||||
jump.label = 'Button 2'
|
||||
jump.status = new_status.id
|
||||
|
||||
jump = new_status.add_action('choice')
|
||||
jump.label = 'Button no target status'
|
||||
|
||||
workflow.add_global_action('Action 1')
|
||||
|
||||
interactive_action = workflow.add_global_action('Interactive action (should not be shown)')
|
||||
interactive_action.add_action('form')
|
||||
|
||||
workflow.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert resp.form['button_name'].options == [
|
||||
('Action 1', False, 'Action 1'),
|
||||
('Button 1', False, 'Button 1'),
|
||||
('Button 2', False, 'Button 2'),
|
||||
('Button 4 (not available)', True, 'Button 4 (not available)'),
|
||||
]
|
||||
|
||||
resp.form['button_name'] = 'Button 1'
|
||||
resp.form['who'] = 'submitter'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert escape('Click on "Button 1" by submitter') in resp.text
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
resp.form['who'] = 'other'
|
||||
resp.form['who_id'] = user.test_uuid
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert escape('Click on "Button 1" by test user') in resp.text
|
||||
|
||||
user.remove_self()
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert escape('Click on "Button 1" by missing user') in resp.text
|
||||
|
||||
user.store()
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
resp.form['who'] = 'receiver'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert escape('Selected user is "Backoffice user" but it is not defined.') in resp.text
|
||||
|
||||
resp = resp.click('Open test options')
|
||||
resp.form['agent'] = user.test_uuid
|
||||
resp.form.submit().follow()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert escape('Selected user is "Backoffice user" but it is not defined.') not in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_status(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertStatus(id='1', status_name='Deleted status'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
|
||||
assert resp.form['status_name'].options == [
|
||||
('Just Submitted', False, 'Just Submitted'),
|
||||
('New', False, 'New'),
|
||||
('Rejected', False, 'Rejected'),
|
||||
('Accepted', False, 'Accepted'),
|
||||
('Finished', False, 'Finished'),
|
||||
('Deleted status (not available)', False, 'Deleted status (not available)'),
|
||||
]
|
||||
|
||||
|
||||
def test_workflow_tests_action_skip_time(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.SkipTime(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
|
||||
resp.form['seconds'] = '1 day 1 hour 1 minute'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert TestDef.get(testdef.id).workflow_tests.actions[0].seconds == 25 * 60 * 60 + 60
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert resp.form['seconds'].value == '1 day, 1 hour and 1 minute'
|
||||
|
||||
resp = resp.form.submit().follow()
|
||||
assert TestDef.get(testdef.id).workflow_tests.actions[0].seconds == 25 * 60 * 60 + 60
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_email(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertEmail(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'not configured' not in resp.text
|
||||
assert 'Email to' not in resp.text
|
||||
|
||||
# empty configuration is allowed
|
||||
resp = resp.click('Edit')
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
resp = resp.click('Edit')
|
||||
resp.form['subject_strings$element0'] = 'abc'
|
||||
resp.form['body_strings$element0'] = 'def'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'Email to' not in resp.text
|
||||
|
||||
assert_email = TestDef.get(testdef.id).workflow_tests.actions[0]
|
||||
assert assert_email.subject_strings == ['abc']
|
||||
assert assert_email.body_strings == ['def']
|
||||
|
||||
resp = resp.click('Edit')
|
||||
resp.form['addresses$element0'] = 'test@entrouvert.com'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert escape('Email to "test@entrouvert.com"') in resp.text
|
||||
|
||||
assert_email.addresses = ['a@entrouvert.com', 'b@entrouvert.com', 'c@entrouvert.com']
|
||||
assert_email.parent.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert escape('Email to "a@entrouvert.com" (+2)') in resp.text
|
||||
|
||||
assert_email.addresses = []
|
||||
assert_email.subject_strings = ['Hello your form has been submitted']
|
||||
assert_email.parent.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert escape('Subject must contain "Hello your form has been su(…)"') in resp.text
|
||||
|
||||
assert_email.subject_strings = []
|
||||
assert_email.body_strings = ['Hello your form has been submitted']
|
||||
assert_email.parent.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert escape('Body must contain "Hello your form has been su(…)"') in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_sms(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertSMS(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'not configured' not in resp.text
|
||||
assert 'SMS to' not in resp.text
|
||||
|
||||
# empty configuration is allowed
|
||||
resp = resp.click('Edit')
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
resp = resp.click('Edit')
|
||||
resp.form['phone_numbers$element0'] = '0123456789'
|
||||
resp.form['body'] = 'Hello your form has been submitted'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'SMS to 0123456789' in resp.text
|
||||
|
||||
assert_sms = TestDef.get(testdef.id).workflow_tests.actions[0]
|
||||
assert assert_sms.phone_numbers == ['0123456789']
|
||||
assert assert_sms.body == 'Hello your form has been submitted'
|
||||
|
||||
assert_sms.phone_numbers = ['0123456789', '0123456781', '0123456782']
|
||||
assert_sms.parent.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert escape('SMS to 0123456789 (+2)') in resp.text
|
||||
|
||||
assert_sms.phone_numbers = []
|
||||
assert_sms.parent.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'Hello your form has been su(…)' in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_anonymise(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertAnonymise(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'Edit' not in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_redirect(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertRedirect(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'not configured' in resp.text
|
||||
|
||||
resp = resp.click('Edit')
|
||||
resp.form['url'] = 'http://example.com'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'not configured' not in resp.text
|
||||
assert 'http://example.com' in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_history_message(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertHistoryMessage(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'not configured' in resp.text
|
||||
|
||||
resp = resp.click('Edit')
|
||||
resp.form['message'] = 'Hello your form has been submitted'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'not configured' not in resp.text
|
||||
assert 'Hello your form has been su(…)' in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_alert(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertAlert(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'not configured' in resp.text
|
||||
|
||||
resp = resp.click('Edit')
|
||||
resp.form['message'] = 'Hello your form has been submitted'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'not configured' not in resp.text
|
||||
assert 'Hello your form has been su(…)' in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_criticality(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
workflow.add_status(name='New status')
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertCriticality(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
assert 'not configured' in resp.text
|
||||
|
||||
resp = resp.click('Edit')
|
||||
assert 'Workflow has no criticality levels.' in resp.text
|
||||
|
||||
workflow.criticality_levels = [
|
||||
WorkflowCriticalityLevel(name='green'),
|
||||
WorkflowCriticalityLevel(name='red'),
|
||||
]
|
||||
workflow.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
resp.form['level_id'].select(text='green')
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'not configured' not in resp.text
|
||||
assert escape('Criticality is "green"') in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_backoffice_field(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
workflow.add_status(name='New status')
|
||||
|
||||
workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow)
|
||||
workflow.backoffice_fields_formdef.fields = [
|
||||
fields.StringField(id='bo1', label='Text'),
|
||||
fields.StringField(id='bo2', label='Text 2'),
|
||||
]
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow = workflow
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertBackofficeFieldValues(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert resp.form['fields$element0$field_id'].options == [
|
||||
('', False, ''),
|
||||
('bo1', False, 'Text - Text (line)'),
|
||||
('bo2', False, 'Text 2 - Text (line)'),
|
||||
]
|
||||
|
||||
resp.form['fields$element0$field_id'] = 'bo2'
|
||||
resp.form['fields$element0$value'] = 'xxx'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert_bakoffice_field_values = TestDef.get(testdef.id).workflow_tests.actions[0]
|
||||
assert assert_bakoffice_field_values.fields == [
|
||||
{'field_id': 'bo2', 'value': 'xxx'},
|
||||
]
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert resp.form['fields$element0$field_id'].value == 'bo2'
|
||||
assert resp.form['fields$element0$value'].value == 'xxx'
|
||||
|
||||
|
||||
def test_workflow_tests_action_assert_webservice_call(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertWebserviceCall(id='1'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert 'you must define corresponding webservice response' in resp.text
|
||||
|
||||
resp = resp.click('Add webservice response')
|
||||
assert 'There are no webservice responses yet.' in resp.text
|
||||
|
||||
response = WebserviceResponse()
|
||||
response.testdef_id = testdef.id
|
||||
response.name = 'Fake response'
|
||||
response.store()
|
||||
|
||||
response2 = WebserviceResponse()
|
||||
response2.testdef_id = testdef.id
|
||||
response2.name = 'Fake response 2'
|
||||
response2.store()
|
||||
|
||||
response3 = WebserviceResponse()
|
||||
response3.testdef_id = testdef.id + 1
|
||||
response3.name = 'Other response'
|
||||
response3.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/1/' % testdef.id)
|
||||
assert resp.form['webservice_response_uuid'].options == [
|
||||
(str(response.uuid), False, 'Fake response'),
|
||||
(str(response2.uuid), False, 'Fake response 2'),
|
||||
]
|
||||
assert resp.form['call_count'].value == '1'
|
||||
|
||||
resp.form['webservice_response_uuid'] = response.uuid
|
||||
resp.form['call_count'] = 2
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert 'Fake response' in resp.text
|
||||
assert 'Broken' not in resp.text
|
||||
|
||||
assert_webservice_call = TestDef.get(testdef.id).workflow_tests.actions[0]
|
||||
assert assert_webservice_call.webservice_response_uuid == response.uuid
|
||||
assert assert_webservice_call.call_count == 2
|
||||
|
||||
response.remove_self()
|
||||
resp = app.get('/backoffice/forms/1/tests/%s/workflow/' % testdef.id)
|
||||
|
||||
assert 'Broken, missing webservice response' in resp.text
|
||||
assert 'Fake response' not in resp.text
|
||||
|
||||
|
||||
def test_workflow_tests_actions_reorder(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.store()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.name = 'First test'
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(id='0', button_name='First'),
|
||||
workflow_tests.ButtonClick(id='1', button_name='Second'),
|
||||
workflow_tests.ButtonClick(id='2', button_name='Third'),
|
||||
workflow_tests.ButtonClick(id='3', button_name='Fourth'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
url = '/backoffice/forms/%s/tests/%s/workflow/update_order' % (formdef.id, testdef.id)
|
||||
|
||||
# missing element in params: do nothing
|
||||
resp = app.get(url + '?order=0;3;1;2;')
|
||||
assert resp.json == {'success': 'ko'}
|
||||
|
||||
# missing order in params: do nothing
|
||||
resp = app.get(url + '?element=0')
|
||||
assert resp.json == {'success': 'ko'}
|
||||
|
||||
resp = app.get(url + '?order=0;3;1;2;&element=3')
|
||||
assert resp.json == {'success': 'ok'}
|
||||
testdef = TestDef.get(testdef.id)
|
||||
assert [x.id for x in testdef.workflow_tests.actions] == ['0', '3', '1', '2']
|
||||
|
||||
# unknown id: ignored
|
||||
resp = app.get(url + '?order=0;1;2;3;4;&element=3')
|
||||
assert resp.json == {'success': 'ok'}
|
||||
testdef = TestDef.get(testdef.id)
|
||||
assert [x.id for x in testdef.workflow_tests.actions] == ['0', '1', '2', '3']
|
||||
|
||||
# missing id: do nothing
|
||||
resp = app.get(url + '?order=0;3;1;&element=3')
|
||||
assert resp.json == {'success': 'ko'}
|
||||
testdef = TestDef.get(testdef.id)
|
||||
assert [x.id for x in testdef.workflow_tests.actions] == ['0', '1', '2', '3']
|
||||
|
||||
|
||||
def test_workflow_tests_run(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
role = pub.role_class(name='test role')
|
||||
role.store()
|
||||
|
||||
test_user = pub.user_class(name='test user')
|
||||
test_user.email = 'test@example.com'
|
||||
test_user.test_uuid = '42'
|
||||
test_user.roles = [role.id]
|
||||
test_user.store()
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
|
||||
sendmail = new_status.add_action('sendmail')
|
||||
sendmail.to = ['test@example.org']
|
||||
sendmail.subject = 'Hello'
|
||||
sendmail.body = 'abc'
|
||||
|
||||
jump = new_status.add_action('choice')
|
||||
jump.label = 'Loop on status'
|
||||
jump.status = new_status.id
|
||||
jump.by = [role.id]
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdata)
|
||||
testdef.agent_id = test_user.test_uuid
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.ButtonClick(id='1', button_name='Loop on status'),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/results/')
|
||||
resp = resp.click('Run tests').follow()
|
||||
|
||||
assert len(resp.pyquery('tr')) == 1
|
||||
assert 'Success!' in resp.text
|
||||
|
||||
# change button label
|
||||
jump.label = 'xxx'
|
||||
workflow.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/results/')
|
||||
resp = resp.click('Run tests').follow()
|
||||
|
||||
assert escape('Workflow error: Button "Loop on status" is not displayed.') in resp.text
|
||||
|
||||
resp = resp.click('Display details')
|
||||
|
||||
assert 'Form status when error occured: New status' in resp.text
|
||||
assert resp.pyquery('li#test-action').text() == 'Test action: Simulate click on action button'
|
||||
assert (
|
||||
resp.pyquery('li#test-action a').attr('href')
|
||||
== 'http://example.net/backoffice/forms/1/tests/%s/workflow/#1' % testdef.id
|
||||
)
|
||||
|
||||
testdef.workflow_tests.actions = []
|
||||
testdef.store()
|
||||
|
||||
resp = app.get(resp.request.url)
|
||||
assert 'Form status when error occured: New status' in resp.text
|
||||
assert resp.pyquery('li#test-action').text() == 'Test action: deleted'
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertEmail(id='1', body_strings=['def']),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/results/')
|
||||
resp = resp.click('Run tests').follow()
|
||||
assert escape('Email body does not contain "def".') in resp.text
|
||||
|
||||
resp = resp.click('Display details')
|
||||
assert 'Form status when error occured: New status' in resp.text
|
||||
assert 'Email body: \nabc' in resp.text
|
||||
assert resp.pyquery('li#test-action').text() == 'Test action: Assert email is sent'
|
||||
|
||||
|
||||
def test_workflow_tests_run_webservice_call(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
|
||||
wscall = new_status.add_action('webservice_call')
|
||||
wscall.url = 'http://example.com/json'
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'test title'
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.store()
|
||||
|
||||
testdef = TestDef.create_from_formdata(formdef, formdef.data_class()())
|
||||
testdef.store()
|
||||
|
||||
response = WebserviceResponse()
|
||||
response.testdef_id = testdef.id
|
||||
response.name = 'Fake response'
|
||||
response.url = 'http://example.com/json'
|
||||
response.payload = '{}'
|
||||
response.store()
|
||||
|
||||
testdef.workflow_tests.actions = [
|
||||
workflow_tests.AssertWebserviceCall(webservice_response_uuid=response.uuid, call_count=1),
|
||||
]
|
||||
testdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/results/run').follow()
|
||||
assert 'Success!' in resp.text
|
||||
|
||||
wscall.response_type = 'attachment'
|
||||
workflow.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/1/tests/results/run').follow()
|
||||
assert 'Workflow error: Webservice response Fake response was used 0 times' in resp.text
|
||||
|
||||
|
||||
def test_workfow_tests_creation_from_formdata(pub):
|
||||
create_superuser(pub)
|
||||
|
||||
workflow = Workflow(name='Workflow One')
|
||||
new_status = workflow.add_status(name='New status')
|
||||
end_status = workflow.add_status(name='End status')
|
||||
|
||||
jump = new_status.add_action('jump')
|
||||
jump.status = end_status.id
|
||||
|
||||
workflow.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.workflow_id = workflow.id
|
||||
formdef.name = 'test title'
|
||||
formdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
formdata.receipt_time = make_aware(datetime.datetime(2022, 1, 1, 0, 0))
|
||||
formdata.store()
|
||||
formdata.perform_workflow()
|
||||
formdata.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/%s/tests/new' % formdef.id)
|
||||
resp.form['name'] = 'First test'
|
||||
resp.form['creation_mode'] = 'formdata-wf'
|
||||
resp.form['formdata'].select(text='1-1 - Unknown User - 2022-01-01 00:00')
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
testdef = TestDef.select()[0]
|
||||
assert len(testdef.workflow_tests.actions) == 1
|
||||
assert testdef.workflow_tests.actions[0].key == 'assert-status'
|
||||
assert testdef.workflow_tests.actions[0].status_name == 'End status'
|