Compare commits

..

218 Commits

Author SHA1 Message Date
Frédéric Péters 094cae52e5 tox: add build dependencies (#36515)
gitea-wip/wcs/pipeline/head There was a failure building this commit Details
gitea/wcs/pipeline/head Build started... Details
2019-11-19 16:01:40 +01:00
Frédéric Péters 00e130ae53 tox: get a newer pytest version for py3 build (#36515) 2019-11-19 16:01:40 +01:00
Frédéric Péters b5c262dc64 jenkins: limit notification errors to myself (#36515) 2019-11-19 16:01:04 +01:00
Frédéric Péters 731697ebc9 jenkins: force clean git (#36515) 2019-11-19 16:01:03 +01:00
Frédéric Péters cb17e126b7 tox: build py3 (#36515) 2019-11-19 16:01:02 +01:00
Frédéric Péters a87a181371 publisher: restore config.json from zip file as text (#36515) 2019-11-19 16:01:00 +01:00
Frédéric Péters 7a29de02bb tests: get test_finish_interrupted_request responses content (#36515) 2019-11-19 16:00:59 +01:00
Frédéric Péters f1b90ce1e2 tests: make test_finish_failed_request work standalone (#36515) 2019-11-19 16:00:58 +01:00
Frédéric Péters 57f96ee3b4 misc: use force_str to encode exception in error page (#36515) 2019-11-19 16:00:57 +01:00
Frédéric Péters 7b0978cf00 publisher: only handle non-encodable exception strings in python 2 (#36515) 2019-11-19 16:00:55 +01:00
Frédéric Péters 4b9c230141 misc: don't enable form tokens in simulated form in test_display_form (#36515) 2019-11-19 16:00:53 +01:00
Frédéric Péters 4f3dc60311 formdef: add pre-json serialization of struct_time workflow options (#36515)
(python3 json serializer would otherwise catch them and serialize them
as tuples)
2019-11-19 16:00:49 +01:00
Frédéric Péters 4f488c1d57 tests: use PicklableUpload in test that requires copying structure (#36515) 2019-11-19 16:00:48 +01:00
Frédéric Péters 9cf9355463 tests: don't check application/msword extension as it may vary (#36515) 2019-11-19 16:00:46 +01:00
Frédéric Péters 03987a6e6c forms: mark invalid prefill value explicitely (#36515) 2019-11-19 16:00:45 +01:00
Frédéric Péters 625b574280 tests: update check against json exception message (#36515) 2019-11-19 16:00:44 +01:00
Frédéric Péters 7dcd2d485b tests: mark request as POST to get mock form data parsed (#36515) 2019-11-19 16:00:42 +01:00
Frédéric Péters 52df31918f ctl: update check_hobos to use force_str to handle strings (#36515) 2019-11-19 16:00:40 +01:00
Frédéric Péters 4aa96b93c5 portfolio: pass text to json (#36515) 2019-11-19 16:00:39 +01:00
Frédéric Péters 0be6a94f13 runscript: pass script name as str (#36515) 2019-11-19 16:00:37 +01:00
Frédéric Péters 0eb5415bee workflows: look for existence of json content with private attribute (#36515) 2019-11-19 16:00:35 +01:00
Frédéric Péters e9771cc834 storage: force (some) filename as bytes (#36515)
(required for python 3.5)
2019-11-19 16:00:34 +01:00
Frédéric Péters a1414e1ebc form: use force_str on rendered map (#36515) 2019-11-19 16:00:33 +01:00
Frédéric Péters 478ff982a1 misc: sort dictionary keys when exporting to xml (#36515) 2019-11-19 16:00:31 +01:00
Frédéric Péters 82fd9489bf emails: pass button link into force_str (#36515) 2019-11-19 16:00:30 +01:00
Frédéric Péters 88d47903d1 tests: update error email for new py3 exception value (#36515) 2019-11-19 16:00:29 +01:00
Frédéric Péters a923e056ea tests: check decoded email payload (#36515) 2019-11-19 16:00:27 +01:00
Frédéric Péters 2877790b24 emails: force_str() on html rendition of email (#36515) 2019-11-19 16:00:26 +01:00
Frédéric Péters 3145fd7367 tests: update test_form_table_field_submit to use force_str (#36515) 2019-11-19 16:00:23 +01:00
Frédéric Péters 1bb05944f8 tests: use force_str() to encode item options (#36515) 2019-11-19 16:00:22 +01:00
Frédéric Péters 54d4b4686b misc: adapt ranked items for py3 (#36515) 2019-11-19 16:00:20 +01:00
Frédéric Péters 21dd276479 tests: adapt mime type tests (#36515) 2019-11-19 16:00:19 +01:00
Frédéric Péters 12094de8e4 tests: only check simplify() on bytes in py2 (#36515) 2019-11-19 16:00:18 +01:00
Frédéric Péters 859683f690 tests: adapt test_json_str_decoder for py3 (#36515) 2019-11-19 16:00:17 +01:00
Frédéric Péters a55daeec35 misc: update ranked items csv value for py3 (#36515) 2019-11-19 16:00:15 +01:00
Frédéric Péters b48283f626 tests: adapt inspect page test for py3 (#36515) 2019-11-19 16:00:14 +01:00
Frédéric Péters 30a9d2d4c3 form: use force_str() on wysiwyg fields (#36515) 2019-11-19 16:00:13 +01:00
Frédéric Péters e45c19e794 workflows: check for json request using private attribute (#36515) 2019-11-19 16:00:11 +01:00
Frédéric Péters badde34b2e misc: adapt password storage (#36515) 2019-11-19 16:00:10 +01:00
Frédéric Péters 191f01bd7e tests: don't manually encode utf8 in formdef name (#36515) 2019-11-19 16:00:08 +01:00
Frédéric Péters 762560fcaf tests: don't distinguish unicode data sources in py3 (#36515) 2019-11-19 16:00:07 +01:00
Frédéric Péters 228053c9ce logger: use next() to iterate over log lines (#36515) 2019-11-19 16:00:06 +01:00
Frédéric Péters 158d6fac2d idp: read metadata and PEM keys as text (#36515) 2019-11-19 16:00:05 +01:00
Frédéric Péters 207d3ae91e tests: check user formdef xml export using bytes (#36515) 2019-11-19 16:00:03 +01:00
Frédéric Péters fdb0ad1aa1 admin: use string to pass overwriting form xml (#36515) 2019-11-19 16:00:02 +01:00
Frédéric Péters 1e8ca52e3c misc: adapt password checking to py3 (#36515) 2019-11-19 16:00:01 +01:00
Frédéric Péters 00fc3deef0 idp: always write files as binaries (#36515) 2019-11-19 16:00:00 +01:00
Frédéric Péters f4ad46acc6 storage: allow sorting objects with missing (receipt_)time (#36515) 2019-11-19 15:59:58 +01:00
Frédéric Péters 370357e40a middleware: iterate over a copy of form keys when adding session variables (#36515) 2019-11-19 15:59:57 +01:00
Frédéric Péters 3fefe4a344 franceconnect: adapt to py3 (#36515) 2019-11-19 15:59:55 +01:00
Frédéric Péters 78084aa023 ctl: open zip file in binary mode (#36515) 2019-11-19 15:59:54 +01:00
Frédéric Péters 7a446f5c88 admin: use binary files for theme/global export/import (#36515) 2019-11-19 15:59:52 +01:00
Frédéric Péters 1591530889 workflows: open generated pdf file in binary mode (#36515) 2019-11-19 15:59:51 +01:00
Frédéric Péters 8e52fc8814 backoffice: export ods as binary (#36515) 2019-11-19 15:59:50 +01:00
Frédéric Péters b3bc038a2e api: alter request user using hidden attribute (#36515) 2019-11-19 15:59:48 +01:00
Frédéric Péters e0e128de4b tests: don't pass encoding to json.dumps (#36515) 2019-11-19 15:59:47 +01:00
Frédéric Péters 1ec58ee869 misc: pass bytes to base64 in utils.attachment() function (#36515) 2019-11-19 15:59:46 +01:00
Frédéric Péters 96bf98cd7c formdef: handle base64 in xml import/export (#36515) 2019-11-19 15:59:45 +01:00
Frédéric Péters a33e8ff8b4 misc: pass bytes to base64 when doing http basic authentication (#36515) 2019-11-19 15:59:43 +01:00
Frédéric Péters 6875bf8954 misc: use strings when distributing base64 to json (#36515) 2019-11-19 15:59:42 +01:00
Frédéric Péters 72bd7af167 workflows: use json_loads wrapper when displaying wscall error details (#36515) 2019-11-19 15:59:40 +01:00
Frédéric Péters dabe208677 tests: check generated PDF using bytes (#36515) 2019-11-19 15:59:39 +01:00
Frédéric Péters 5793e5c509 formdef: use itertools.chain to iterate over two lists (#36515) 2019-11-19 15:59:38 +01:00
Frédéric Péters 8158b456fa tests: use bytes for attachments (#36515) 2019-11-19 15:59:37 +01:00
Frédéric Péters d62e945441 tests: check download contents as bytes (#36515) 2019-11-19 15:59:35 +01:00
Frédéric Péters 9b2e2c8d13 data sources: use bytes to generate hash for cache key (#36515) 2019-11-19 15:59:34 +01:00
Frédéric Péters ed9cdb777c tests: write invalid json file using codecs.encode (#36515) 2019-11-19 15:59:32 +01:00
Frédéric Péters 844dea5853 commands: adapt convert-to-sql for python3 (#36515) 2019-11-19 15:59:31 +01:00
Frédéric Péters 31cc721510 tests: check convert to sql errors using str() (#36515) 2019-11-19 15:59:30 +01:00
Frédéric Péters 8c1fc39764 ctl: use bytes in check_hobos command (#36515) 2019-11-19 15:58:57 +01:00
Frédéric Péters b401245d4b ctl: use open() instead of file() (#36515) 2019-11-19 15:58:54 +01:00
Frédéric Péters 87e940ddfc tests: check provisioned user attribute as proper type (#36515) 2019-11-19 15:58:53 +01:00
Frédéric Péters 248e7563c7 tests: expand get_visited_objects result (#36515) 2019-11-19 15:58:51 +01:00
Frédéric Péters 9f9f623cb4 tests: only check integer part of reproj result (#36515) 2019-11-19 15:58:49 +01:00
Frédéric Péters a68dccf8dd misc: adapt nir code validation to py3 (#36515) 2019-11-19 15:58:47 +01:00
Frédéric Péters dc82d0f918 workflows: use force_str to display wscall error details (#36515) 2019-11-19 15:58:46 +01:00
Frédéric Péters 21ee4427c6 tests: check for different error message on py3 (#36515) 2019-11-19 15:58:44 +01:00
Frédéric Péters b1fa54eb33 workflows: save webservice call response as bytes (#36515) 2019-11-19 15:58:42 +01:00
Frédéric Péters 6c89239f3d fields: force base64 data as bytes (#36515) 2019-11-19 15:58:41 +01:00
Frédéric Péters 129306e47d workflows: update export to models for py3 (#36515) 2019-11-19 15:58:39 +01:00
Frédéric Péters 85a4024417 workflows: process model text as strings (#36515) 2019-11-19 15:58:36 +01:00
Frédéric Péters 091ba4ae15 workflows: handle rtf as text (#36515) 2019-11-19 15:58:33 +01:00
Frédéric Péters 81f2f7d90c misc: encode bytes when producing json output (#36515) 2019-11-19 15:58:32 +01:00
Frédéric Péters 7d329c856b conditions: adapt encoding of validation errors (#36515) 2019-11-19 15:58:26 +01:00
Frédéric Péters d0b9ff274c backoffice: expand geolocation as list (#36515) 2019-11-19 15:58:25 +01:00
Frédéric Péters a4b8063976 franceconnect: pass bytes to hashlib (#36515) 2019-11-19 15:58:23 +01:00
Frédéric Péters 5a7161bbb5 sessions: expand .items() when marking visited objects (#36515) 2019-11-19 15:58:22 +01:00
Frédéric Péters fca37a3205 hobo notify: use force_str on provisioned attributes (#36515) 2019-11-19 15:58:20 +01:00
Frédéric Péters 4c6c08dcae tests: check content as bytes (#36515) 2019-11-19 15:58:19 +01:00
Frédéric Péters 10d4451122 workflows: read attachments as binaries (#36515) 2019-11-19 15:58:15 +01:00
Frédéric Péters 3444d4bec8 storage: unpickle python2 strings as bytes (#36515)
(it would be so much easier to pass encoding='utf-8' and have them
converted but this will fail for objects with datetime/date/time
instances as those requires encoding='latin-1)
2019-11-19 15:58:05 +01:00
Frédéric Péters d0b2407738 misc: don't encode json in local charset in python 3 (#36515) 2019-11-19 15:58:04 +01:00
Frédéric Péters 5f8c243cd0 request: rework json parsing for quixote3 (#36515) 2019-11-19 15:57:53 +01:00
Frédéric Péters 7523343893 misc: load json as text (#36515) 2019-11-19 15:57:52 +01:00
Frédéric Péters c3057f76bd workflows: expand items as list (#36515) 2019-11-19 15:57:50 +01:00
Frédéric Péters ca35deba02 storage: add support for sorting disparate types (#36515) 2019-11-19 15:57:47 +01:00
Frédéric Péters c089fbe70c wscalls: use force_str when importing wscalls (#36515) 2019-11-19 15:57:39 +01:00
Frédéric Péters 937b2c538d use force_str when initializing site from xml (#36515) 2019-11-19 15:57:38 +01:00
Frédéric Péters 4f7d4cd6cf data sources: import xml as strings (#36515) 2019-11-19 15:57:37 +01:00
Frédéric Péters 75c18872dc backoffice: expand categories as list (#36515) 2019-11-19 15:57:35 +01:00
Frédéric Péters 95d29ec088 backoffice: use force_str for query parameter (#36515) 2019-11-19 15:57:34 +01:00
Frédéric Péters 03f2634895 saml: read keys as binaries (#36515) 2019-11-19 15:57:31 +01:00
Frédéric Péters 3938a8ab87 sessions: write files as binaries (#36515) 2019-11-19 15:57:30 +01:00
Frédéric Péters d1a7e6e187 tests: expand .values() as lists (#36515) 2019-11-19 15:57:29 +01:00
Frédéric Péters b97f9a63f0 tests: rewrite home keywords test to handle different attributes order (#36515) 2019-11-19 15:57:27 +01:00
Frédéric Péters 1356365a15 lazy: add __bool__ = __nonzero__ for py3 compatibility (#36515) 2019-11-19 15:57:25 +01:00
Frédéric Péters 2cd20e666d formdata: iterate over a copy of dict keys when flattening it (#36515) 2019-11-19 15:57:24 +01:00
Frédéric Péters 48118f8781 misc: adapt de/encoding in import zip (#36515) 2019-11-19 15:57:22 +01:00
Frédéric Péters 4322fe9f64 tests: use __name__ to get function name (#36515) 2019-11-19 15:57:21 +01:00
Frédéric Péters e0bdd1c60f admin: always use absolute imports for qommon (#36515) 2019-11-19 15:57:19 +01:00
Frédéric Péters 23ab03be8a tests: expand iterkeys() in assert (#36515) 2019-11-19 15:57:18 +01:00
Frédéric Péters a97b93d163 misc: always expand lists used as widget options (#36515) 2019-11-19 15:57:16 +01:00
Frédéric Péters 71f4a0fd3e admin: update workflow functions sort for python3 (#36515) 2019-11-19 15:57:14 +01:00
Frédéric Péters 5395f9255f workflows: force visibility to be a list() (#36515) 2019-11-19 15:57:13 +01:00
Frédéric Péters af22690d31 tests: always compare roles as sets (#36515) 2019-11-19 15:57:11 +01:00
Frédéric Péters e4f335a604 misc: load wscall json response as text (#36515) 2019-11-19 15:57:08 +01:00
Frédéric Péters f5880908bd tests: update http requests mocking to use bytes (#36515) 2019-11-19 15:57:05 +01:00
Frédéric Péters 0f5d4555d2 misc: update requests code for py3 (#36515) 2019-11-19 15:57:03 +01:00
Frédéric Péters f8587a00e1 admin: adapt theme handling (#36515) 2019-11-19 15:57:01 +01:00
Frédéric Péters 9cb2e4cc43 admin: add correct encoding to user search (#36515) 2019-11-19 15:56:59 +01:00
Frédéric Péters 8831c91a51 admin: add required encoding for graphviz subprocess (#36515) 2019-11-19 15:56:57 +01:00
Frédéric Péters 697949b551 tests: make first admin workflows test run standalone (#36515) 2019-11-19 15:56:55 +01:00
Frédéric Péters 8f3419baf1 tests: check category export/import as bytes (#36515) 2019-11-19 15:56:53 +01:00
Frédéric Péters 0e120fb4d8 workflows: use force_str on wscall result (#36515) 2019-11-19 15:56:51 +01:00
Frédéric Péters 654d3d3b91 workflows: expand geolocation list (#36515) 2019-11-19 15:56:46 +01:00
Frédéric Péters e43bba68f5 workflows: handle models as bytes (#36515) 2019-11-19 15:56:45 +01:00
Frédéric Péters dbbb116552 tests: adapt wscall error message check for python 3 (#36515) 2019-11-19 15:56:43 +01:00
Frédéric Péters ec09c2ea6a workflows: allow building an attachment from strings (#36515) 2019-11-19 15:56:42 +01:00
Frédéric Péters 7071ec96c4 tests: use strings in email mocking (#36515) 2019-11-19 15:56:40 +01:00
Frédéric Péters 1bdd4cfda2 tests: pass bytes to base64 (#36515) 2019-11-19 15:56:37 +01:00
Frédéric Péters 0e01b626b1 misc: don't recurse in attachments proxy when deepcopying (#36515) 2019-11-19 15:56:36 +01:00
Frédéric Péters 0a68468493 workflows: expand list of idp to get ws url (#36515) 2019-11-19 15:56:34 +01:00
Frédéric Péters d0b4f24584 workflows: handle encoding in export/import of workflow actions (#36515) 2019-11-19 15:56:33 +01:00
Frédéric Péters ba6e5cd5a1 tests: use bytes in workflow export/import tests (#36515) 2019-11-19 15:56:32 +01:00
Frédéric Péters 2aed9c1bc4 forms: don't decode value in py3 (#36515) 2019-11-19 15:56:30 +01:00
Frédéric Péters 344498ae4d saml: sort IdPs before logging automatically on first one (#36515) 2019-11-19 15:56:29 +01:00
Frédéric Péters 228457be9d misc: encode x509/saml bits (#36515) 2019-11-19 15:56:24 +01:00
Frédéric Péters 4e6f21ef43 misc: do not convert non html body (#36515) 2019-11-19 15:56:22 +01:00
Frédéric Péters 5a4302b461 tests: fix qrcode test so it can be run standalone (#36515) 2019-11-19 15:56:19 +01:00
Frédéric Péters 4b181fc1bf ods: always encode cell data (#36515) 2019-11-19 15:56:17 +01:00
Frédéric Péters fd0b882578 general: always encode json as utf-8 (#36515) 2019-11-19 15:56:08 +01:00
Frédéric Péters e9f5112fbe misc: use force_str for formdef/workflow xml exports (#36515) 2019-11-19 15:56:02 +01:00
Frédéric Péters a231aa0d97 templates: use force_str (#36515) 2019-11-19 15:56:00 +01:00
Frédéric Péters bed2079a3e sql: force visiting_objects to be list (#36515) 2019-11-19 15:55:59 +01:00
Frédéric Péters 259c62a197 backoffice: expand .items() to list (#36515) 2019-11-19 15:55:58 +01:00
Frédéric Péters 82cc9a3a98 api: force keywords to be an expanded list (#36515) 2019-11-19 15:55:57 +01:00
Frédéric Péters 9fe1e84921 api: use sys.maxsize instead of sys.maxint (#36515) 2019-11-19 15:55:55 +01:00
Frédéric Péters 8be31ef846 misc: use force_str in lazy variables (#36515) 2019-11-19 15:55:50 +01:00
Frédéric Péters 53812ecd8e tests: use force_str() in sql fts test (#36515) 2019-11-19 15:55:49 +01:00
Frédéric Péters 7bd1967fc4 tests: expand ranges for comparisons (#36515) 2019-11-19 15:55:48 +01:00
Frédéric Péters 22ba97571f use force_str for formdef/workflow imports (#36515) 2019-11-19 15:55:47 +01:00
Frédéric Péters ec72e371d3 api: compare signature as bytes (#36515) 2019-11-19 15:55:45 +01:00
Frédéric Péters c3fe5545ac misc: add force_str(), to encode in Python 2 (#36515) 2019-11-19 15:55:44 +01:00
Frédéric Péters 30ff9ff561 workflows: only encode on py2 (#36515) 2019-11-19 15:55:41 +01:00
Frédéric Péters e541759a67 misc: use list comprehensions to check for password character classes (#36515) 2019-11-19 15:55:37 +01:00
Frédéric Péters 1987c5407f misc: pass bytes to hmac (#36515) 2019-11-19 15:55:36 +01:00
Frédéric Péters fd88b46128 storage: use bytes when reading chunks (#36515) 2019-11-19 15:55:34 +01:00
Frédéric Péters 5da68239d3 misc: don't encode json data in py3 (#36515) 2019-11-19 15:55:33 +01:00
Frédéric Péters db6fe5f504 tests: use urllib from six (#36515) 2019-11-19 15:55:31 +01:00
Frédéric Péters 4746d75099 workflows: force list before sorting (#36515) 2019-11-19 15:55:30 +01:00
Frédéric Péters 05abc1038d tests: use req._user to force request user (#36515) 2019-11-19 15:55:28 +01:00
Frédéric Péters a295a0b441 tests: give bytes to Upload (#36515) 2019-11-19 15:55:27 +01:00
Frédéric Péters e4333e6719 logger, six.string_types + six.integer_types (#36515) 2019-11-19 15:55:24 +01:00
Frédéric Péters 6d8c2ffb14 misc: replace basestring by six.string_types (#36515) 2019-11-19 15:55:23 +01:00
Frédéric Péters 6f279b7b28 misc: iteritems() -> items() (#36515) 2019-11-19 15:55:20 +01:00
Frédéric Péters ef77a23f79 misc: use six.string_types to check (str, unicode) (#36515) 2019-11-19 15:55:06 +01:00
Frédéric Péters 7d7cea05a6 tests: update logged error test to use integer division (#36515) 2019-11-19 15:55:05 +01:00
Frédéric Péters b79a1f5fbc tests: force integer division in logged error test (#36515) 2019-11-19 15:55:04 +01:00
Frédéric Péters a6e27b1df3 backoffice: use integer division in stats (#36515) 2019-11-19 15:55:02 +01:00
Frédéric Péters cf248b9e5a backoffice: use integer division for pagination (#36515) 2019-11-19 15:55:01 +01:00
Frédéric Péters 5f1629a898 misc: update xml storage to check bytes (#36515) 2019-11-19 15:54:39 +01:00
Frédéric Péters 90fdf2c398 misc: replace new module by types module (#36515) 2019-11-19 15:54:18 +01:00
Frédéric Péters 0f4fbdd7ff form: adapt date/email widget encoding for py3 (#36515) 2019-11-19 15:54:17 +01:00
Frédéric Péters 1a7e2ec81b sql: force view columns to be defined as text (#36515) 2019-11-19 15:54:16 +01:00
Frédéric Péters d174de9cf7 misc: force workflow roles to list (#36515) 2019-11-19 15:54:14 +01:00
Frédéric Péters 95c5c36103 sql: update storage for py3 compatibility (#36515) 2019-11-19 15:54:12 +01:00
Frédéric Péters 07357b8549 request: only preprocess form fields in py2 (#36515) 2019-11-19 15:53:53 +01:00
Frédéric Péters b73c5cc666 settings: write theme files as binaries (#36515) 2019-11-19 15:53:51 +01:00
Frédéric Péters dbec13c73d admin: create archive as bytes (#36515) 2019-11-19 15:53:49 +01:00
Frédéric Péters b46c654323 misc: use binary files for thumbnails (#36515) 2019-11-19 15:53:46 +01:00
Frédéric Péters 8c0658c2c3 misc: write metadata file as binary (#36515) 2019-11-19 15:53:44 +01:00
Frédéric Péters d4e9a16468 form: use binary files for uploads (#36515) 2019-11-19 15:53:42 +01:00
Frédéric Péters 56e9be1142 tests: read/write uploads as binary (#36515) 2019-11-19 15:53:40 +01:00
Frédéric Péters 6d80317832 storage: load/save index files as binary files (#36515) 2019-11-19 15:53:38 +01:00
Frédéric Péters 4fced111d6 misc: use binary file for storage (#36515) 2019-11-19 15:53:36 +01:00
Frédéric Péters 1346c90386 ezt: update bits for python3 (has_key, basestring, number types) (#36515) 2019-11-19 15:53:34 +01:00
Frédéric Péters d4840892b4 misc: update simplify for py3 (#36515) 2019-11-19 15:53:30 +01:00
Frédéric Péters d6bce19689 use absolute import when creating default workflows (#36515) 2019-11-19 15:53:29 +01:00
Frédéric Péters 9dd19f2f05 misc: update config load for python 3 (#36515) 2019-11-19 15:53:27 +01:00
Frédéric Péters 4f827110bf misc: replace has_key usages (#36515) 2019-11-19 15:53:26 +01:00
Frédéric Péters 0db7cb0804 admin: sort emails/texts using key function (#36515) 2019-11-19 15:53:24 +01:00
Frédéric Péters 61efee3db9 tests: sort using key function (#36515) 2019-11-19 15:53:23 +01:00
Frédéric Péters be4101e5a7 api: sort using key function (#36515) 2019-11-19 15:53:21 +01:00
Frédéric Péters 5d7bc70c0f backoffice: sort ids using key function (#36515) 2019-11-19 15:53:20 +01:00
Frédéric Péters a4743c216c misc: sort using key function (#36515) 2019-11-19 15:53:18 +01:00
Frédéric Péters 11ad87508c misc: sort categories using key function (#36515) 2019-11-19 15:53:17 +01:00
Frédéric Péters 2927c87edd storage: sort results using a key, not a cmp function (#36515) 2019-11-19 15:53:14 +01:00
Frédéric Péters 0e968f3b83 misc: get urllib from django.six (#36515) 2019-11-19 15:53:13 +01:00
Frédéric Péters a321f200be misc: get META via request.environ (do not override property) (#36515) 2019-11-19 15:53:11 +01:00
Frédéric Péters 4d39395c39 misc: always pass bytes for md5 hashing (#36515) 2019-11-19 15:53:08 +01:00
Frédéric Péters 48ffcf975a misc: write files as binaries (#36515) 2019-11-19 15:53:05 +01:00
Frédéric Péters 1ea19f1c74 tests: use resp.text (#36515) 2019-11-19 15:53:01 +01:00
Frédéric Péters e4bd408044 tests: use file() to open file (#36515) 2019-11-19 15:52:39 +01:00
Frédéric Péters d33ae185a7 tests: remove usage of urllib2 (#36515) 2019-11-19 15:48:07 +01:00
Frédéric Péters 75516f7b6a misc: get ConfigParser from six (#36515) 2019-11-19 15:48:07 +01:00
Frédéric Péters 78c4a5f640 misc: use print as function (#36515) 2019-11-19 15:48:07 +01:00
Frédéric Péters c586e8262d tests: update location of MIMEText import (#36515) 2019-11-19 15:48:07 +01:00
Frédéric Péters 87253343d4 misc: update except syntax (#36515) 2019-11-19 15:48:07 +01:00
Frédéric Péters a17d189124 tests: replace cPickle by pickle (#36515) 2019-11-19 15:48:07 +01:00
Frédéric Péters 29d0f9e8eb misc: get urlparse from six (#36515) 2019-11-19 15:48:07 +01:00
Frédéric Péters 86a623669a tests: remove unused cPickle import (#36515) 2019-11-19 15:48:07 +01:00
Frédéric Péters 71508015a3 misc: use cPickle only when available (#36515) 2019-11-19 15:48:06 +01:00
Frédéric Péters 650c9c3093 misc: only convert SafeString when running in Python 2 (#36515) 2019-11-19 15:48:06 +01:00
Frédéric Péters 7614f40bcd misc: use SafeText instead of SafeUnicode (#36515) 2019-11-19 15:48:06 +01:00
Frédéric Péters b9c1293608 general: remove bounce processing (#36515) 2019-11-19 15:48:06 +01:00
Frédéric Péters deaf0c34f1 misc: get StringIO from six (#36515) 2019-11-19 15:48:06 +01:00
Frédéric Péters 34a382f03e general: replace unicode() calls by force_text() (#36515) 2019-11-19 15:48:06 +01:00
Frédéric Péters 95bc775346 general: don't mention encoding of XML export (= default as utf-8) (#37574) 2019-11-19 15:48:06 +01:00
858 changed files with 81487 additions and 216674 deletions

View File

@ -1,9 +1,5 @@
[run]
omit = wcs/qommon/vendor/*.py
dynamic_context = test_function
[report]
omit = wcs/qommon/vendor/*.py
[html]
show_contexts = True

View File

@ -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

14
.gitignore vendored
View File

@ -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/

View File

@ -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

29
Jenkinsfile vendored
View File

@ -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 {

View File

@ -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
View File

@ -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)

View File

@ -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>

BIN
data/themes/alto/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -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 &ecirc;tes ici : [breadcrumb]</p>[end]
[body]
</div>
<div id="footer"></div>
</div>
</body>
</html>

270
data/themes/alto/wcs.css Normal file
View File

@ -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;
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0"?>
<theme name="default" version="1.0">
<label>Default</label>
<desc>Default theme</desc>
<author>Frederic Peters &amp; Dotclear Team</author>
</theme>

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

View File

@ -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>

View File

@ -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>

View File

@ -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 %}

View File

@ -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;
}

2
data/web/css/wcs.css Normal file
View File

@ -0,0 +1,2 @@
@import url(sofresh.css);

179
data/webbots Normal file
View File

@ -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

1
debian/compat vendored Normal file
View File

@ -0,0 +1 @@
9

74
debian/control vendored
View File

@ -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

1
debian/copyright vendored
View File

@ -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'.

View File

@ -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')

10
debian/rules vendored
View File

@ -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

7
debian/settings.py vendored
View File

@ -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 = []

32
debian/uwsgi.ini vendored
View File

@ -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

4
debian/wcs-manage vendored
View File

@ -18,8 +18,8 @@ fi
if test $# -eq 0
then
python3 ${MANAGE} help
python ${MANAGE} help
exit 1
fi
python3 ${MANAGE} "$@"
python ${MANAGE} "$@"

1
debian/wcs.cron.d vendored
View File

@ -1,4 +1,3 @@
MAILTO=root
LANG=C.UTF-8
* * * * * wcs /usr/bin/wcs-manage cron

3
debian/wcs.dirs vendored
View File

@ -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

3
debian/wcs.init vendored
View File

@ -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

4
debian/wcs.install vendored
View File

@ -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

11
debian/wcs.postinst vendored
View File

@ -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
;;

7
debian/wcs.service vendored
View File

@ -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]

2
debian/wcs.triggers vendored
View File

@ -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

8
doc/Makefile Normal file
View File

@ -0,0 +1,8 @@
all:
$(MAKE) -C fr
clean:
$(MAKE) -C fr clean
.PHONY: clean

34
doc/fr/Makefile Normal file
View File

@ -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

45
doc/fr/custom.tex Normal file
View File

@ -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

143
doc/fr/default.css Normal file
View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

490
doc/fr/fncychap.sty Normal file
View File

@ -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

469
doc/fr/wcs-admin.rst Normal file
View File

@ -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

5
doc/scripts/removealpha.sh Executable file
View File

@ -0,0 +1,5 @@
#! /bin/sh
size=$(identify $1 | cut -d ' ' -f 3)
composite $1 -size $(identify $1 | cut -d ' ' -f3) xc:white $2

29
doc/scripts/rst2latex.py Executable file
View File

@ -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

View File

@ -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

225
help/fr/api-auth.page Normal file
View File

@ -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&amp;timestamp=' + timestamp + '&amp;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>&amp;algo=<var>algo</var>&amp;timestamp=<var>ts</var>&amp;orig=<var>orig</var>&amp;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 += '&amp;'
new_query += urllib.urlencode((
('algo', algo),
('timestamp', timestamp),
('nonce', nonce)))
if orig is not None:
new_query += '&amp;' + urllib.urlencode({'orig': orig})
signature = base64.b64encode(sign_string(new_query, key, algo=algo))
new_query += '&amp;' + 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&amp;arg2=val2', 'user-key', orig='user')
</code>
</listing>
<listing>
<title>PHP</title>
<code mime="application/x-php">
&lt;?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'] . '&amp;';
}
$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 .= '&amp;' . 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&amp;arg2=val2", "user", "user-key");
?&gt;
</code>
</listing>
<listing>
<title>Shell (bash)</title>
<code mime="application/x-shellscript">
#!/bin/bash
url="http://www.example.net/uri/?arg=val&amp;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&lt;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&amp;timestamp=$now&amp;orig=$orig"
sig=$(rawurlencode $(echo -n "$qs" | openssl dgst -binary -sha256 -hmac "$key" | base64))
signed="${url}?$qs&amp;signature=$sig"
echo "$signed"
</code>
</listing>
</section>
</page>

View File

@ -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 dune fiche</title>
<p>
La création dune fiche se fait par une requête <code>POST</code> à
ladresse <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 lidentifiant non-numérique utilisé dans les URL, il
est visible depuis lécran dun modèle de fiche, dans la fenêtre de
modification du titre.
</p>
</note>
<p>
Lattribut <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>
Lexemple 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 douverture
(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 lOpéra",
"date_ouverture": "2020-11-12",
"nom": "Parking Opéra-Tolozan"
}
}
</code>
<note>
<p>
Il ny a aucune vérification du format des données reçues, elles sont
enregistrée telles quelles. Les contraintes de validation ou les conditions
daffichage ne sont pas prises en compte.
</p>
</note>
</section>
<section id="card-import-csv">
<title>Création dun ensemble de fiches par import CSV</title>
<p>
Il est possible de créer un ensemble de fiches par import dun fichier CSV.
Cela seffectue par une requête <code>PUT</code> à ladresse
<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 dune 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 dun import
CSV dans linterface 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 dexécution de limport peut dépasser la
limite acceptée par le serveur HTTP (souvent 20 ou 30 secondes). Il est donc
recommandé dutiliser loption asynchrone de limport CSV.
</p>
<p>
Pour faire un import asynchrone, ajouter <code>async=on</code> dans les
paramètres de lURL :
</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 nest pas aussitôt importé. Une tâche
(<em>job</em>) est lancée qui va effectivement faire limport, et on peut en
suivre la progression en appellant son URL indiquée en retour de lappel 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 limport, 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 limport s'est correctement
déroulé.
</p>
</section>
</section>
<section id="card">
<title>Récupération des données dune fiche</title>
<p>
Lexemple suivant récupère les données dune 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 lOpé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 lAPI de <link xref="#create"/>.
</p>
</section>
<section id="card-edit">
<title>Modification des données dune fiche</title>
<p>
Sur le même modèle que les formulaires une fiche qui peut être modifiée (par
la présence dune action de workflow de type « Édition ») peut également être
modifiée via un appel à lAPI.
</p>
<p>
Les données attendues sont similaires à la création dune 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 dun 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 lAPI de <link
xref="api-get#listing">récupération dune liste de formulaires</link>. Les
autres paramètres de cette API sont également exploitables, pour inclure
lensemble 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 lidentifiant de celle-ci à ladresse, 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 dun 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>

View File

@ -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.
Ladresse appelée doit répondre aux exigences suivantes :
L'adresse appelée doit répondre aux exigences suivantes :
</p>
<list>
@ -32,7 +32,7 @@ Ladresse 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 @@ Ladresse 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), ladresse 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 dun é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": [
{

View File

@ -12,43 +12,27 @@
</info>
<title>Complétion et modification dun 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 dun formulaire ou la modification dun
structurées permettant la complétion d'un formulaire ou la modification d'un
formulaire existant.
</p>
<note>
<p>
Il ny a aucune vérification du format des données reçues, elles sont
enregistrée telles quelles. Les contraintes de validation ou les conditions
daffichage ne sont pas prises en compte.
</p>
</note>
<section id="create">
<title>Complétion dun formulaire</title>
<title>Complétion d'un formulaire</title>
<p>
La complétion dun formulaire se fait par une requête <code>POST</code> à
ladresse <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 lidentifiant non-numérique utilisé dans les URL, il
est visible depuis lécran dun formulaire, dans la fenêtre de modification
du titre.
</p>
</note>
<p>
Lattribut <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>
Lattribut <code>user</code> est optionnel et peut contenir un identifiant
permettant dassocier la demande à un usager existant; lidentifiant peut
être soit lidentifiant unique (UUID/NameID), passé dans une clé
<code>NameID</code>, soit ladresse électronique de lusager, passée dans
une clé <code>email</code>.
</p>
<p>
Lattribut <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>
Lattribut <code>context</code> est également optionnel et contient une
série de renseignements supplémentaires sur le contexte de lenvoi 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>
Lexemple suivant complète un formulaire dinscription à 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 dun formulaire</title>
<title>Modification d'un formulaire</title>
<p>
Un formulaire qui peut être modifié (par la présence dune action de workflow
de type « Édition ») peut également être modifié via un appel à
lAPI, en faisant un <code>POST</code> sur ladresse 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 dun 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 doptions, 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 lidentifiant dune donnée structurée ou si la
donnée structurée complète est transmise, lidentifiant 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 dune 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 doptions, 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 didentifiants 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 didentifiants doptions</title>
<title>Liste d'identifiants d'options</title>
<code>
"data": {
"<var>varname</var>": ["1", "2"]

View File

@ -12,25 +12,26 @@
</info>
<title>Récupération des données dun formulaire</title>
<title>Récupération des données d'un formulaire</title>
<p>
Il sagit ici dune 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
lapplication tierce (mode pull) ou par w.c.s., à différents moments du
traitement dun 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>
Lexemple suivant récupère les données dun formulaire dinscription à 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 lerreur",
"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 lattribut
<code>geolocations</code>. Pour linstant il nen existe quune 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 lattribut <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 dune capacité
daction sur la demande.
la série <code>actions</code> reprend les rôles disposant d'une capacité
d'action sur la demande.
</p>
<p>
Linformation sur lorigine de la demande, si la saisie a eu lieu depuis le
backoffice et quel était le canal dorigine de la demande, est disponible
dans lattribut <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>
Lhistorique du formulaire, ses transitions dans différents statuts, est disponible dans lattribut
<code>evolution</code>. Cette liste de dictionnaires contient linstant de la transition
dans lattribut <code>time</code>, le code du statut concerné dans <code>status</code> et
une description de lutilisateur responsable de la transition dans <code>user</code>. Lattribut
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 lappel dun <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 dun 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 lappel dun <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, lapplication 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
lexemple ci-dessus).
l'exemple ci-dessus).
</p>
</section>
@ -209,7 +210,7 @@ lexemple ci-dessus).
<title>Types de données</title>
<p>
Les données dun 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 dun 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 sagit des mêmes paramètres que pour lexport 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>&amp;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>&amp;signature…</var></input>
</screen>
<p>
Dautres paramètres de filtres existent. Pour filtrer sur les demandes déposées
après une date donnée
(<code>?filter-start=on&amp;filter-start-value=2020-01-03</code>),
ou avant une date donnée
(<code>?filter-end=on&amp;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&amp;filter-start-mtime-value=2020-01-03</code>
ou <code>?filter-end-mtime=on&amp;filter-end-mtime-value=2020-01-03</code>).
Pour filtrer selon lusager associé (<code>?filter-user-uuid=XYZ</code>) ou
selon lappartenance dun usager à une fonction particulière
(<code>?filter-user-function=_mandataire&amp;filter-user-uuid=XYZ</code>).
Et pour filtrer selon lagent 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 lensemble des données associées aux formulaires soient
retourné, plutôt quun 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
ladresse.
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>&amp;signature…</var></input>
</screen>
<p>
À noter que pour ne pas alourdir lexport en mode <code>full=on</code>, le
contenu des champs de type « Fichier » nest 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 dinclure (<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 dun 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 daccès sont simplifiés à une signature simple, il
nest pas nécessaire de préciser lidentifiant dun 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&amp;anonymise</input>
<input>GET https://www.example.net/api/forms/inscriptions/10/?anonymise</input>
</screen>
<p>
Par ailleurs, lAPI « Liste de formulaires » accepte un paramètre
<code>include-anonymised</code> permettant dinclure (<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&amp;anonymise<var>&amp;signature…</var></input>
<output style="prompt">$ </output><input>curl -H "Accept: application/json" \
https://www.example.net/api/forms/inscriptions/10/?anonymise<var>&amp;signature…</var></input>
</screen>
</section>
<section id="global-data">
<title>Données de lensemble des formulaires</title>
<title>Données de l'ensemble des formulaires</title>
<p>
De manière similaire à lAPI de récupération de la liste des demandes dun
formulaire, il est possible de récupérer lensemble 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 sagit 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>&amp;signature…</var></input>
</screen>
<note><p>
Le paramètre <code>full</code> nest 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 lURL.
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 linterface 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 dobtenir les informations géographiques de
lensemble 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 @@ lensemble des demandes :
<title>Code de suivi</title>
<p>
Une API existe pour déterminer lexistence dun 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 lattribut <code>url</code> se trouvera ladresse native de la demande,
qui demandera authentification de lutilisateur, et dans lattribut
<code>load_url</code> une adresse permettant de charger la demande sur la seule
foi de laccès. Il est important dutiliser 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 dinexistence 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>

View File

@ -15,27 +15,35 @@
<title>Introduction aux API</title>
<p>
Cette section de la documentation sadresse aux développeurs
dapplications 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>
LAPI Web Services est constituée dappels 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
dexemples. Les différentes pages détaillent les points daccè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>

View File

@ -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 à
lURL <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 @@ lURL <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 lURL
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 dobtenir 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 dune clé <code>count</code>.
</p>
<note style="important">
<p>Linformation <code>count</code> nest <em>pas</em> le décompte intégral des
demandes, cest 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 à lURL <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 à lURL <code>/api/categories/</code>
</screen>
<p>
Il est possible de passer un paramètre <code>full=on</code> dans ladresse pour
obtenir pour chacune des catégories la liste des formulaires quelle 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 dune catégorie précise sont disponibles à lURL
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 largument
<code>?backoffice-submission=on</code> à cette URL, pour nobtenir 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 à lURL <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 à lURL <code>/api/roles</code>.
<section id="data-schema">
<title>Schéma de données dun formulaire</title>
<title>Schéma de données d'un formulaire</title>
<p>
Le schéma de données dun formulaire est accessible à ladresse
<code>/api/formdefs/<em>slug</em>/schema</code>; lappel 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> à ladresse 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>

View File

@ -8,28 +8,61 @@
<name>Frédéric Péters</name>
<email>fpeters@entrouvert.com</email>
</credit>
<desc>Demandes et brouillons dun usager</desc>
<desc>Profil utilisateur, formulaires associés, etc.</desc>
</info>
<title>Récupération des données dun usager</title>
<title>Récupération des données d'un utilisateur</title>
<p>
Il sagit 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 à lURL
<code>/api/users/<var>uuid</var>/forms</code>, elle reprend un ensemble
minimal dinformations 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 dinscription",
"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 dinscription",
"name": "Demande d'inscription",
"status": "Nouveau",
"title": "Demande dinscription #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 dadresse",
"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 dadresse",
"name": "Changement d'adresse",
"status": "Traitement de la demande termin\u00e9",
"title": "Changement dadresse #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 lauthentification HTTP Basique;
elle demande la mise en place de lalgorithme 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> à ladresse. 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 sappliquer 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 lusager est accessible à ladresse
<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>&amp;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>

View File

@ -12,16 +12,16 @@
</info>
<title>Traitement dun 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 lappel.
conditionnés par le résultat de l'appel.
</p>
</section>
@ -33,34 +33,37 @@ conditionnés par le résultat de lappel.
<p>
Pour des traitements asynchrones, w.c.s expose également une API autorisant
les logiciels tiers à faire progresser le
traitement dun formulaire; cela passe par la définition dans le statut du
workflow dun é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 dun changement détat se fait par une requête <code>POST</code> à
ladresse du formulaire en question, suivi de <code>jump/trigger/</code> et de
la référence à lidentifiant de déclencheur (<code>validate</code> dans
lexemple qui suit).
</p>
<p>
Lors de cette requête, il est nécessaire dinclure lentê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 daccompagner le déclenchement dun changement
de statut dune 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>

View File

@ -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 lusager (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 ny 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 lensemble des instances.
disponibles depuis l'ensemble des instances.
</p>
</note>

View File

@ -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.
Lutilisation de ces vues est recommandée, laccè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 lidentifiant du formulaire et <var>libellé</var>
étant son nom tel quil 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 lutilisateur</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 dentré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 nest 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
lhistorique 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 à lensemble des formulaires, cest-à-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 dune 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 dopérations sur
lensemble des formulaires.
Ces agrégations sont utiles pour permettre la réalisation d'opérations sur
l'ensemble des formulaires.
</p>
<screen>

View File

@ -14,25 +14,25 @@
<title>Géolocalisation</title>
<p>
Un champ de type « Carte » permet dafficher à lusager 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 à louverture du formulaire, ainsi que le niveau de zoom initial,
parce quil 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 lusager dans un zoom affichant la terre
entière, ou le moindre brin dherbe.
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
à lappareil mobile de lusager 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é dautomatiquement sélectionner
comme point la position courante de lusager.
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 lusager 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 dadresse 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>

View File

@ -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 sagit 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é à
lusager dans lindicateur 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 limportance de la définition dune page comme premier
élément de formulaire, un message dinformation est affiché en haut de la
définition des champs quand ce nest 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 dun formulaire nont 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 daccès à un parking si lusager a noté dans
une page précédente quil 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 dune option permettant den conditionner
laffichage.
« Page » disposent d'une option permettant d'en conditionner
l'affichage.
</p>
<p>
Une condition sexprime sous forme dune <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 à lusager 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 laccès au parking ne devrait
pas être affichée pour les personnes ayant précisés quelles venaient à
pieds, le champ serait complété avec lexpression 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>
Dautres 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>

View File

@ -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 à lIdentique 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 ladapter à 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 ladapter à 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 doptions
plus simples aux plus complexes; une variété de possibilités et d'options
existent.
</p>
</section>
@ -41,7 +41,7 @@ permettant de ladapter à 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 dactions
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 ladapter à de nombreux usages différents.
</info>
<title>Paramétrage système</title>
<p>
Pour ladministration système, <app>w.c.s.</app> dispose dune 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
dinformation.
d'information.
</p>
</section>

View File

@ -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 dexprimer une condition sous forme dune
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 dexemples 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 dexemples concrets, rencontrés dans le paramétrage.
<title>Expressions simples</title>
<p>
Pour tester le code postal associé à lutilisateur courant, par exemple
pour proposer une page différente aux habitants dune commune particulière,
lexpression 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 didé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 dexemples concrets, rencontrés dans le paramétrage.
<p>
De manière générale, la négative peut également être obtenue en entourant
lexpression dun <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 dexemples 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 linscription
est faite pour plusieurs personnes. Lutilisation 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 dexemples 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 lapplication 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 dexemples 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 dexemples 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 dexemples 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 dun 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) &gt;= 31</code></example>

View File

@ -14,14 +14,14 @@
<title>Variables de substitution</title>
<p>
<app>w.c.s.</app> dispose dun 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, dun formulaire, dun 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 dun
courriel ou dans la définition dune logique de traitement dun 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>Lidentifiant 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>Ladresse 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>Ladresse 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 lheure 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 lutilisateur 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>Ladresse électronique de lutilisateur 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>
À lintérieur dune 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>Lintitulé 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>Lidentifiant 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
ladministrateur système, via des variables denvironnement ou la
configuration de linstance 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 lheure 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 dadresse) 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>Ladresse 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>Ladresse 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, sil 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 lutilisateur (demandeur)</title>
<title>Informations sur l'utilisateur (demandeur)</title>
<p>
Les données contiennent aussi des informations sur lutilisateur 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 lutilisateur</p></td>
<td><p>Le nom de l'utilisateur</p></td>
</tr>
<tr>
<td><p><code>form_user_email</code></p></td>
<td><p>Ladresse électronique de lutilisateur</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 lutilisateur (NameID), pour le fournisseur
didentité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 dun 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>Lensemble 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 dun 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 lidentifiant 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 dautres 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 lusager contient une série dinformations 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 dy ajouter de nouvelles données par lintermédiaire de
liens contenant des paramètres. Cela permet par exemple dinclure une URL
personnalisée dans un courriel vers lusager 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
ladministrateur 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>

View File

@ -13,9 +13,9 @@
<title>Mécanique de gabarits</title>
<p>
De nombreux éléments permettent lutilisation dun système simple permettant
de générer du contenu variant suivant certaines données. Lexemple le plus
simple peut être le contenu dun courriel, dans lequel lutilisateur 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>
À lusage, 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 dafficher 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
dune 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 dune 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é dun <code>{% else %}</code>.
possibilité d'un <code>{% else %}</code>.
</p>
<example>
<code>
Pour toute information complémentaire, nhé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 dinformations 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 lutilisation 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.

View File

@ -11,24 +11,24 @@
<desc>Quelques options</desc>
</info>
<title>Variables denvironnement</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 sagit dinformations proches de ladministration
système, il existe également la possibilité dutiliser des variables
denvironnement.
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
denvironnement <code>QOMMON_MAIL_REDIRECTION</code>.
d'environnement <code>QOMMON_MAIL_REDIRECTION</code>.
</p>
</section>
@ -38,9 +38,9 @@ denvironnement.
<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 denvironnement <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 dune 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>

View File

@ -8,57 +8,64 @@
<name>Frédéric Péters</name>
<email>fpeters@entrouvert.com</email>
</credit>
<desc>Autorisations daccè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 dadministration</title>
<title>Permissions d'administration</title>
<p>
Dans le fonctionnement de base un compte dadministration ouvre laccès à
toutes les pages de linterface dadministration, il est néanmoins possible
de paramétrer de manière plus fine laccè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 lespace de paramétrage, dans la section « Sécurité », suivez le lien
« Permissions dadministration ». Pour chacune des grandes sections de
ladministration (<gui>Formulaires</gui>, <gui>Modèles de fiches</gui>,
<gui>Workflows</gui>…) vous pouvez restreindre laccè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 dadministration de secours</title>
<title>Accès administrateur de secours</title>
<p>
En cas de mauvaise manipulation et de perte totale de laccès à linterface
dadministration, le système dispose dun moyen de secours
pour temporairement désactiver la vérification des permissions daccè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 linstance (<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 ladresse 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 &gt; ADMIN_FOR_ALL</input>
</screen>
<p>
Linterface dadministration 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
laccè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 laccè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>

View File

@ -14,18 +14,18 @@
<title>Anonymisation</title>
<p>
Dans le circuit de traitement dune demande, après quelle ait été traitée,
il peut être souhaité den 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 lexpiration dun 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>
Lanonymisation 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>

View File

@ -12,25 +12,25 @@
</info>
<title>Affichage dun formulaire</title>
<title>Affichage d'un formulaire</title>
<p>
Lors du traitement dune demande il peut être nécessaire de demander des
informations supplémentaires à lusager, ou quun agent complète la demande
avec des informations internes (à limage dun « cadre réservé à
ladministration » 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 dun formulaire de renseignement dinformations
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>
Linterface 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, à lexception 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.
Ladresse 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>

View File

@ -15,13 +15,13 @@
<p>
Une fois la géolocalisation activée pour un formulaire, le workflow associé
peut faire appel à laction 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 dune adresse ou
en les extrayant dun 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, linformation 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 dune 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 dun 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 dun
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 dune photographie</title>
<title>Extraction d'une photographie</title>
<p>
Le paramètre est une expression pointant une variable tirée dun 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>

View File

@ -14,15 +14,15 @@
<title>Changement de statut automatique</title>
<p>
Laction de changement de statut automatique permet de passer automatiquement
un formulaire dun 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
linteraction avec des systèmes externes, et un délai dexpiration, 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 lAPI, 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 dexpiration permet davoir 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 dexpression Python, en préfixant
celle-ci dun 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>

View File

@ -14,7 +14,7 @@
<title>Variables de workflow</title>
<p>
Il arrive souvent quun 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 quil existe de variations mais ça entraîne rapidement un coût dentretien
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 dun 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 sagit de définir une série de champs. Il
y a quand même une différence, lors de la définition dun 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 dun formulaire la variable serait remplie avec "Deux
places de cinéma" et dans le traitement, une action denvoi 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 davoir une liste à choix multiple comme
option de workflow, reprenant par exemple les types denvoi 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 lexemple précédent du coucours, lentièreté du
contenu du courriel pourrait relever dune 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 loption de workflow à lélément denvoi de courriel.
associer l'option de workflow à l'élément d'envoi de courriel.
</p>
<p>

View File

@ -14,12 +14,12 @@
<title>Appel à un webservice</title>
<p>
Cette action permet dappeler 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 ladresse 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 dURL. 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 dURL ne peuvent être que des chaînes, si ce nest 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
lensemble 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
lAPI</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 dun 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 nest indiquée et que le formulaire ne doit pas être transmis,
alors la requête HTTP effectuée est un GET sur lURL.
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 denregistrer 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 lenvoyer 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 lappel 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 sassurer 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> :

38
jenkins.sh Executable file
View File

@ -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

View File

@ -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
View File

@ -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

View File

@ -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
View File

@ -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)

View File

@ -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]

View File

@ -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/'

View File

@ -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 &quot;form_var_…&quot; detected. Please replace it by &quot;block_var_…&quot;.'
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'},
]

File diff suppressed because it is too large Load Diff

View File

@ -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']

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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 == []

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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)

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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'

Some files were not shown because too many files have changed in this diff Show More