nautilus-scripts : compression de PDF
[progfou.git] / wcs / wcs-dynexport
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 """
4 Outil d'export dynamique de données w.c.s.
5
6 Copyright : Agence universitaire de la Francophonie — www.auf.org
7 Licence : GNU General Public Licence, version 2
8 Auteur : Jean Christophe André
9 Date de création : 13 mars 2013
10
11 Depends: wcs, python-simplejson, python-magic
12
13 URL d'accès :
14 - /dynexport => liste des formulaires pour le domaine courant
15 - /dynexport/domains.json => liste des domaines disponibles
16 - /dynexport/formulaire => liste des options ci-dessous
17 - /dynexport/formulaire/fields.json
18 - /dynexport/formulaire/field-names.json
19 - /dynexport/formulaire/field-names.txt
20 - /dynexport/formulaire/data.json
21 - /dynexport/formulaire/last-run.log
22 - /dynexport/formulaire/liste-dossiers.json
23 - /dynexport/formulaire/clear-cache => vide le cache
24 - /dynexport/formulaire/data/nom-dossier.json
25 - /dynexport/formulaire/data/nom-dossier_attachement-1.xxx
26 - /dynexport/formulaire/data/nom-dossier_attachement-2.xxx
27 - /dynexport/formulaire/data/nom-dossier_attachement-…
28 """
29 import sys
30 import os
31 import os.path
32 import logging
33 import time # time, gmtime, strftime, strptime, struct_time
34 import simplejson as json
35 import magic
36 import mimetypes
37 import unicodedata
38 from cStringIO import StringIO
39 from gzip import GzipFile
40 from re import match
41
42 EXPIRE_DELAY = 5 # maximum 5 secondes en cache web
43 TIME_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' # format date pour HTTP
44 #ETABLISSEMENT_FORMAT = r'^(\w+\s-\s.+)\s\(\d+\s-\s(Nord|Sud)\)$'
45
46 WCS_ROOT_DIR = '/var/lib/wcs'
47 WCS_DOMAIN_SUFFIX = '.auf.org'
48 WCS_CACHE_DIR = '/var/tmp'
49 WCS_CACHE_DELAY_DEFAULT = 7*24*60*60 # 1 semaine
50 WCS_CACHE_DELAY_FORMS = 5*60 # 5 minutes
51
52 #--------------------------------------------------------------------------
53 # variables globales
54 #--------------------------------------------------------------------------
55
56 pub = None
57
58 #--------------------------------------------------------------------------
59 # fonctions de traitement
60 #--------------------------------------------------------------------------
61
62 def http_redirect(location, code='302'):
63     headers = {}
64     headers['Content-Type'] = 'text/plain; charset=utf-8'
65     headers['Status'] = '302 Redirection'
66     headers['Location'] = location
67     data = """If you see this, it means the automatic redirection has failed.
68 Please go to ${location}"""
69     # envoi de la réponse
70     headers = ''.join(map(lambda x: "%s: %s\r\n" % (x, headers[x]), headers))
71     f = open('/dev/stdout', 'wb')
72     f.write(headers + "\r\n")
73     if data:
74         f.write(data)
75     f.flush()
76     # arrêt du traitement
77     sys.exit(0)
78
79 def http_reply_and_exit(data, mime_type='text/html', charset='utf-8'):
80     if data is None: data = ''
81     # références horaires
82     current_time = time.time()
83     mtime = time.gmtime(current_time)
84     etime = time.gmtime(current_time + EXPIRE_DELAY)
85     if os.environ.has_key('HTTP_IF_MODIFIED_SINCE'):
86         try:
87             itime = time.strptime(os.environ['HTTP_IF_MODIFIED_SINCE'], TIME_FORMAT)
88         except ValueError:
89             itime = None
90     else:
91         itime = None
92     # préparation des en-têtes et données
93     headers = {}
94     headers['Content-Type'] = '%s; charset=%s' % (mime_type, charset)
95     headers['Last-Modified'] = time.strftime(TIME_FORMAT, mtime)
96     headers['Expires'] = time.strftime(TIME_FORMAT, etime)
97     if os.environ['REQUEST_METHOD'] == 'GET' and (not itime or mtime > itime):
98         # détermination de la version demandée (compressée ou non)
99         if os.environ.get('HTTP_ACCEPT_ENCODING','').split(',').count('gzip') > 0:
100             zdata = StringIO()
101             GzipFile('', 'w', 9, zdata).write(data)
102             data = zdata.getvalue()
103             headers['Content-Encoding'] = 'gzip'
104         headers['Vary'] = 'Content-Encoding'
105         headers['Content-Length'] = len(data)
106     else:
107         data = None
108     # envoi de la réponse
109     headers = ''.join(map(lambda x: "%s: %s\r\n" % (x, headers[x]), headers))
110     f = open('/dev/stdout', 'wb')
111     f.write(headers + "\r\n")
112     if data:
113         f.write(data)
114     f.flush()
115     # arrêt du traitement
116     sys.exit(0)
117
118
119 def _reduce_to_alnum(s, replacement_char='-'):
120     """réduction d'une chaîne de caractères à de l'alpha-numérique"""
121
122     if type(s) is not unicode:
123         s = unicode(s, 'utf-8')
124     s = unicodedata.normalize('NFKD', s).encode('ASCII', 'ignore')
125     r = ''
126     for c in s:
127         if ('a' <= c.lower() <= 'z') or ('0' <= c <= '9'):
128             r += c
129         elif len(r) > 0 and r[-1] != replacement_char:
130             r += replacement_char
131         else: # r == '' or r[-1] == replacement_char
132             pass
133     return r.strip(replacement_char)
134
135 def _make_wcs_cache_name(domain, form, name):
136     return 'wcs-%s-%s-%s' % (domain, form, name)
137
138 def set_wcs_cache(domain, form, name, data, delay=WCS_CACHE_DELAY_DEFAULT):
139     os.umask(0022)
140     cache_filename = _make_wcs_cache_name(domain, form, name)
141     cache_filename = os.path.join(WCS_CACHE_DIR, cache_filename)
142     f = open(cache_filename, 'wb')
143     f.write(data)
144     f.close()
145     # la date de modification est utilisée comme date d'expiration
146     atime = time.time()
147     mtime = atime + delay
148     os.utime(cache_filename, (atime, mtime))
149
150 def get_wcs_cache(domain, form, name):
151     data = None
152     cache_filename = _make_wcs_cache_name(domain, form, name)
153     cache_filename = os.path.join(WCS_CACHE_DIR, cache_filename)
154     if os.path.exists(cache_filename):
155         # la date de modification est utilisée comme date d'expiration
156         if time.time() < os.path.getmtime(cache_filename):
157             data = open(cache_filename, 'rb').read()
158         else:
159             os.unlink(cache_filename)
160     return data
161
162 def clear_wcs_cache(domain, form):
163     cache_filename = _make_wcs_cache_name(domain, form, '')
164     for f in os.listdir(WCS_CACHE_DIR):
165         if f.startswith(cache_filename):
166             os.unlink(os.path.join(WCS_CACHE_DIR, f))
167
168 def set_wcs_publisher(domain):
169     global pub
170     if pub is None:
171         from wcs import publisher
172         pub = publisher.WcsPublisher.create_publisher()
173         pub.app_dir = os.path.join(pub.app_dir, domain)
174         pub.set_config()
175
176 def get_wcs_domains():
177     root = WCS_ROOT_DIR
178     suffix = WCS_DOMAIN_SUFFIX
179     try:
180         l = os.listdir(root)
181     except OSError:
182         return None
183     return [x for x in l if os.path.isdir(os.path.join(root, x)) and x.endswith(suffix)]
184
185 def get_wcs_forms(domain):
186     """extraction de la liste des formulaires"""
187     data = get_wcs_cache(domain, 'ALL', 'ALL.json')
188     if data is not None:
189         return json.loads(data, encoding='utf-8')
190     set_wcs_publisher(domain)
191     from wcs.formdef import FormDef
192     forms = [f.url_name for i,f in FormDef.items()]
193     data = json.dumps(forms, ensure_ascii=False).encode('utf-8')
194     set_wcs_cache(domain, 'ALL', 'ALL.json', data, WCS_CACHE_DELAY_FORMS)
195     return forms
196
197 def get_wcs_form_data(domain, form):
198     """extraction des données du formulaire"""
199     data = get_wcs_cache(domain, form, 'metadata.json')
200     if data is not None:
201         return json.loads(data, encoding='utf-8')
202     # dictionnaire des metadonnées (qui seront mises en cache)
203     metadata = {}
204
205     os.umask(0022)
206     logname = _make_wcs_cache_name(domain, form, 'last-run.log')
207     logger = logging.getLogger('wcs-dynexport')
208     logger.setLevel(logging.DEBUG)
209     log_formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
210     log_handler = logging.FileHandler(os.path.join(WCS_CACHE_DIR, logname))
211     log_handler.setLevel(logging.DEBUG)
212     log_handler.setFormatter(log_formatter)
213     logger.addHandler(log_handler)
214
215     logger.info('Début.')
216     log_handler.flush()
217
218     set_wcs_publisher(domain)
219     from wcs.formdef import FormDef
220     from wcs.fields import TitleField, CommentField, TextField, \
221                            StringField, ItemField, ItemsField, EmailField, \
222                            DateField, FileField, BoolField, TableField
223     formdef = FormDef.get_by_urlname(form)
224
225     # nommage des champs de façon unique
226     fields = {}
227     field_names = {}
228     field_names_duplicates = {}
229     for i, field in enumerate(formdef.fields):
230         if isinstance(field, TitleField) or isinstance(field, CommentField):
231             continue
232         if field.varname:
233             name = field.varname
234         else:
235             name = _reduce_to_alnum(field.label,'_').lower()
236         if name in field_names.values(): # duplicat
237             field_names_duplicates[name] = field_names_duplicates.get(name, 1) + 1
238             name = '%s_%d' % (name, field_names_duplicates[name])
239         field_names.update({field.id: name})
240         fields.update({field.id: {'index': i, 'name': field_names[field.id], 'label': field.label, 'varname': field.varname and field.varname or ''}})
241
242     data = json.dumps(fields, ensure_ascii=False).encode('utf-8')
243     set_wcs_cache(domain, form, 'fields.json', data)
244     metadata.update({'fields': fields})
245
246     # on charge la base des types MIME une fois pour toutes
247     #magicmime = magic.Magic(mime=True) => ce sera pour plus tard…
248     magicmime = magic.open(magic.MAGIC_MIME)
249     magicmime.load()
250
251     liste_dossiers = []
252     liste_attachements = {}
253     for object in formdef.data_class().select():
254         if object.user is None:
255             logger.warning("Dossier '%s' sans utilisateur associé ?!?"\
256                             " On ignore...", object.id)
257             continue
258
259         try:
260             workflow_status = object.status.startswith('wf-') and \
261                             object.get_workflow_status().name or None
262         except:
263             workflow_status = None
264
265         result = {
266             'num_dossier': object.id,
267             'wcs_status': object.status,
268             'wcs_workflow_status': workflow_status,
269             'wcs_user_email': object.user.email,
270             'wcs_user_display_name': object.user.display_name,
271            #'wcs_last_modified': time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(object.last_modified())),
272             'wcs_comments': [],
273         }
274
275         if object.evolution is not None:
276             for e in object.evolution:
277                 if e.comment is not None:
278                     try:
279                         who = pub.user_class.get(e.who).display_name
280                     except:
281                         who = 'Inconnu(e)'
282                     e_time = time.strftime('%Y-%m-%d %H:%M:%S', e.time)
283                     comment = '%s -- %s %s' % (e.comment, who, e_time)
284                     result['wcs_comments'].append(comment)
285
286         qfiles = { }
287         for field in formdef.fields:
288             field_id = str(field.id)
289             if not field_id in object.data:
290                 continue
291             if isinstance(field, TitleField) or isinstance(field, CommentField):
292                 continue
293             field_name = fields[field_id]['name']
294             data = object.data.get(field_id)
295             # paliatif aux corrections de formulaires en cours de route
296             # (compensation nécessaire pour l'import depuis Sigma 2)
297             if data is None and field.required:
298                 if isinstance(field, StringField) \
299                 or isinstance(field, TextField) \
300                 or isinstance(field, EmailField) \
301                 or isinstance(field, ItemField):
302                     result[field_name] = '(vide)'
303                 elif isinstance(field, ItemsField) \
304                   or isinstance(field, TableField):
305                     result[field_name] = '(vide)'
306                 elif isinstance(field, BoolField):
307                     result[field_name] = False
308                 elif isinstance(field, DateField):
309                     result[field_name] = "9999-12-31"
310                 continue
311             if data is None:
312                 result[field_name] = None
313                 continue
314             if isinstance(field, StringField) or isinstance(field, TextField) \
315             or isinstance(field, EmailField) or isinstance(field, ItemField):
316                 # nettoyage du nom d'établissement (suppression id et Nord/Sud)
317                 #m = match(ETABLISSEMENT_FORMAT, data)
318                 #if m is not None:
319                 #    data = m.groups()[0]
320                 result[field_name] = data
321             elif isinstance(field, ItemsField) or isinstance(field, TableField):
322                 result[field_name] = data # liste => peux-être joindre sur ';'
323             elif isinstance(field, BoolField):
324                 result[field_name] = (data == 'True')
325             elif isinstance(field, DateField):
326                 if isinstance(data, time.struct_time):
327                     result[field_name] = '%04d-%02d-%02d' % (data.tm_year,
328                                                     data.tm_mon, data.tm_mday)
329                 else:
330                     result[field_name] = data
331             elif isinstance(field, FileField):
332                 if '.' in data.orig_filename:
333                     extension = data.orig_filename.rpartition('.')[2].lower()
334                 else: # il n'y a pas d'extension dans le nom de fichier
335                     p = os.path.join(pub.app_dir, 'uploads', data.qfilename)
336                     try:
337                         #m = magicmime.from_file(p) => ce sera pour plus tard…
338                         m = magicmime.file(p).split()[0].strip(';')
339                         extension = mimetypes.guess_extension(m)
340                     except:
341                         logger.warning("Type de fichier inconnu pour '%s'.", p)
342                         extension = None
343                     if extension is not None:
344                         extension = extension[1:]
345                     else:
346                         extension = 'unknown'
347                 result[field_name] = "%s.%s" % (field_name, extension)
348                 qfiles[field_name] = data.qfilename
349             else:
350                 logger.warning("Type de champ inconnu '%s' pour '%s' (%s).",
351                             field.__class__.__name__, field_name, field.label)
352
353         num_dossier = result['num_dossier']
354         nom = _reduce_to_alnum(result.get('nom','sans-nom')).upper()
355         prenom = _reduce_to_alnum(result.get('prenom','sans-prenom')).upper()
356         adel = result.get('adresse_electronique','sans-adel').replace('@','-').lower()
357
358         filename = "%04d-%s-%s-%s" % (num_dossier, nom, prenom, adel)
359         liste_dossiers.append(filename + '.json')
360
361         # sauvegarde des chemins d'accès aux fichiers joints
362         for f in qfiles:
363             dst = filename + '_' + result[f]
364             src = os.path.join(pub.app_dir, 'uploads', qfiles[f])
365             liste_attachements.update({dst: src})
366             # on renomme le fichier joint indiqué dans le dossier
367             result[f] = dst
368
369         # génération du fichier JSON
370         data = json.dumps(result, ensure_ascii=False).encode('utf-8')
371         set_wcs_cache(domain, form, 'data_%s.json' % filename, data)
372
373         logger.info("Dossier '%s' : %s.",
374                                     filename, result['wcs_workflow_status'])
375
376     data = json.dumps(liste_attachements, ensure_ascii=False).encode('utf-8')
377     set_wcs_cache(domain, form, 'data-files.json', data)
378     metadata.update({'attachements': liste_attachements})
379
380     liste_dossiers.sort()
381     data = json.dumps(liste_dossiers, ensure_ascii=False).encode('utf-8')
382     set_wcs_cache(domain, form, 'liste-dossiers.json', data)
383     metadata.update({'dossiers': liste_dossiers})
384
385     logger.info('Fin.')
386     log_handler.flush()
387
388     data = json.dumps(metadata, ensure_ascii=False).encode('utf-8')
389     set_wcs_cache(domain, form, 'metadata.json', data)
390
391 #if __name__ == '__main__':
392 #    try:
393 #        extract_data(formdef, OUTPUT_DIRECTORY)
394 #    except:
395 #        logger.exception("Interruption du traitement pour cause d'erreur !")
396
397 #--------------------------------------------------------------------------
398 # gestion des requêtes web
399 #--------------------------------------------------------------------------
400
401 #l = []
402 #for k in sorted(os.environ):
403 #    l.append('%s=%s\n' % (k, os.environ[k]))
404 #data = ''.join(l)
405 #http_reply_and_exit(data, 'text/plain')
406
407 domain = os.environ.get('HTTP_HOST', '')
408 if domain not in get_wcs_domains():
409     http_reply_and_exit("Domaine '%s' inconnu." % domain, 'text/plain')
410
411 path_info = os.environ.get('PATH_INFO', '')
412
413 path_prefix = os.environ.get('REQUEST_URI', '')
414 if len(path_info) > 0:
415     path_prefix = path_prefix[:-len(path_info)]
416
417 if path_info == '':
418     http_redirect(path_prefix + '/')
419
420 if path_info == '/':
421     # liste des formulaires disponibles
422     l = sorted(get_wcs_forms(domain))
423     l = ['<li><a href="%s/">%s</a></li>' % (f, f) for f in l]
424     title = '<p>Liste des formulaires disponibles&nbsp;:</p>\n'
425     data = '<html>\n' + title + '<ul>\n' + '\n'.join(l) + '\n</ul>\n</html>'
426     http_reply_and_exit(data, 'text/html')
427
428 if path_info == '/index.json':
429     # liste des formulaires disponibles
430     l = sorted(get_wcs_forms(domain))
431     data = json.dumps(l, ensure_ascii=False, indent=' ').encode('utf-8')
432     http_reply_and_exit(data, 'application/json')
433
434 if path_info == '/domains.json':
435     # liste des domaines disponibles
436     l = get_wcs_domains()
437     data = json.dumps(l, ensure_ascii=False, indent=' ').encode('utf-8')
438     http_reply_and_exit(data, 'application/json')
439
440 if match(r'^/[a-z0-9-]+$', path_info):
441     http_redirect(path_prefix + path_info + '/')
442
443 if match(r'^/[a-z0-9-]+/$', path_info):
444     form = path_info.split('/')[1]
445     if form not in get_wcs_forms(domain):
446         http_reply_and_exit("Formulaire '%s' inconnu." % form, 'text/plain')
447     l = [ 'fields.json', 'field-names.json', 'field-names.txt', 'last-run.log', 'liste-dossiers.json' ]
448     l = ['<li><a href="%s">%s</a></li>' % (f, f) for f in l]
449     title = '<p>Liste des informations disponibles&nbsp;:</p>\n'
450     action1 = """<p><a href="data/">Export des données</a></p>\n"""
451     action2 = """<p><a href="clear-cache">Suppression du cache</a> (pour ré-export)</p>\n"""
452     data = '<html>\n' + title + '<ul>\n' + '\n'.join(l) + '\n</ul>\n' + action1 + action2 + '</html>'
453     http_reply_and_exit(data, 'text/html')
454
455 if match(r'^/[a-z0-9-]+/index.json$', path_info):
456     form = path_info.split('/')[1]
457     if form not in get_wcs_forms(domain):
458         http_reply_and_exit("Formulaire '%s' inconnu." % form, 'text/plain')
459     l = [ 'fields.json', 'field-names.json', 'field-names.txt', 'last-run.log', 'liste-dossiers.json', 'data', 'clear-cache' ]
460     data = json.dumps(l, ensure_ascii=False, indent=' ').encode('utf-8')
461     http_reply_and_exit(data, 'application/json')
462
463 if match(r'^/[a-z0-9-]+/clear-cache$', path_info):
464     form = path_info.split('/')[1]
465     if form not in get_wcs_forms(domain):
466         http_reply_and_exit("Formulaire '%s' inconnu." % form, 'text/plain')
467     clear_wcs_cache(domain, form)
468     http_reply_and_exit('Ok.', 'text/plain')
469
470 if match(r'^/[a-z0-9-]+/fields.json$', path_info):
471     form = path_info.split('/')[1]
472     if form not in get_wcs_forms(domain):
473         http_reply_and_exit("Formulaire '%s' inconnu." % form, 'text/plain')
474     get_wcs_form_data(domain, form)
475     d = json.loads(get_wcs_cache(domain, form, 'fields.json'), encoding='utf-8')
476     data = json.dumps(d, ensure_ascii=False, indent=' ').encode('utf-8')
477     http_reply_and_exit(data, 'application/json')
478
479 if match(r'^/[a-z0-9-]+/field-names.json$', path_info):
480     form = path_info.split('/')[1]
481     if form not in get_wcs_forms(domain):
482         http_reply_and_exit("Formulaire '%s' inconnu." % form, 'text/plain')
483     get_wcs_form_data(domain, form)
484     d = json.loads(get_wcs_cache(domain, form, 'fields.json'), encoding='utf-8')
485     d = dict([(k, d[k]['name']) for k in d])
486     data = json.dumps(d, ensure_ascii=False, indent=' ').encode('utf-8')
487     http_reply_and_exit(data, 'application/json')
488
489 if match(r'^/[a-z0-9-]+/field-names.txt$', path_info):
490     form = path_info.split('/')[1]
491     if form not in get_wcs_forms(domain):
492         http_reply_and_exit("Formulaire '%s' inconnu." % form, 'text/plain')
493     get_wcs_form_data(domain, form)
494     d = json.loads(get_wcs_cache(domain, form, 'fields.json'), encoding='utf-8')
495     d = [(k, d[k]['name'], d[k]['label']) for k in d]
496     d = sorted(d, key=lambda x: int(x[0]))
497     text = u''.join([u'%s:%s:%s\n' % (x[0], x[1], x[2]) for x in d])
498     data = text.encode('utf-8')
499     http_reply_and_exit(data, 'text/plain')
500
501 if match(r'^/[a-z0-9-]+/last-run.log$', path_info):
502     form = path_info.split('/')[1]
503     if form not in get_wcs_forms(domain):
504         http_reply_and_exit("Formulaire '%s' inconnu." % form, 'text/plain')
505     get_wcs_form_data(domain, form)
506     data = get_wcs_cache(domain, form, 'last-run.log')
507     http_reply_and_exit(data, 'text/plain')
508
509 if match(r'^/[a-z0-9-]+/liste-dossiers.json$', path_info):
510     form = path_info.split('/')[1]
511     if form not in get_wcs_forms(domain):
512         http_reply_and_exit("Formulaire '%s' inconnu." % form, 'text/plain')
513     get_wcs_form_data(domain, form)
514     data = json.loads(get_wcs_cache(domain, form, 'liste-dossiers.json'), encoding='utf-8')
515     data = json.dumps(data, ensure_ascii=False, indent=' ').encode('utf-8')
516     http_reply_and_exit(data, 'application/json')
517
518 if match(r'^/[a-z0-9-]+/data$', path_info):
519     http_redirect(path_prefix + path_info + '/')
520
521 if match(r'^/[a-z0-9-]+/data/$', path_info):
522     form = path_info.split('/')[1]
523     if form not in get_wcs_forms(domain):
524         http_reply_and_exit("Formulaire '%s' inconnu." % form, 'text/plain')
525     get_wcs_form_data(domain, form)
526     dossiers = json.loads(get_wcs_cache(domain, form, 'liste-dossiers.json'), encoding='utf-8')
527     attachements = json.loads(get_wcs_cache(domain, form, 'data-files.json'), encoding='utf-8')
528     l = sorted(dossiers + attachements.keys())
529     if len(l) > 0:
530         l = ['<li><a href="%s">%s</a></li>' % (f, f) for f in l]
531         title = '<p>Liste des documents disponibles&nbsp;:</p>\n'
532         data = '<html>\n' + title + '<ul>\n' + '\n'.join(l) + '\n</ul>\n</html>'
533     else:
534         data = '<html>\n<p>Aucun document disponible.</p>\n</html>'
535     http_reply_and_exit(data, 'text/html')
536
537 if match(r'^/[a-z0-9-]+/data/index.json$', path_info):
538     form = path_info.split('/')[1]
539     if form not in get_wcs_forms(domain):
540         http_reply_and_exit("Formulaire '%s' inconnu." % form, 'text/plain')
541     get_wcs_form_data(domain, form)
542     dossiers = json.loads(get_wcs_cache(domain, form, 'liste-dossiers.json'), encoding='utf-8')
543     attachements = json.loads(get_wcs_cache(domain, form, 'data-files.json'), encoding='utf-8')
544     l = sorted(dossiers + attachements.keys())
545     data = json.dumps(l, ensure_ascii=False, indent=' ').encode('utf-8')
546     http_reply_and_exit(data, 'application/json')
547
548 if match(r'^/[a-z0-9-]+/data/[^/]+$', path_info):
549     form = path_info.split('/')[1]
550     if form not in get_wcs_forms(domain):
551         http_reply_and_exit("Formulaire '%s' inconnu." % form, 'text/plain')
552     get_wcs_form_data(domain, form)
553     doc = path_info.split('/')[3]
554     dossiers = json.loads(get_wcs_cache(domain, form, 'liste-dossiers.json'), encoding='utf-8')
555     if doc in dossiers:
556         data = get_wcs_cache(domain, form, 'data_' + doc)
557         data = json.loads(data, encoding='utf-8')
558         data = json.dumps(data, ensure_ascii=False, indent=' ').encode('utf-8')
559         http_reply_and_exit(data, 'application/json')
560     attachements = json.loads(get_wcs_cache(domain, form, 'data-files.json'), encoding='utf-8')
561     if doc in attachements:
562         data = open(attachements[doc], 'rb').read()
563         mime_type = mimetypes.guess_type(doc)[0]
564         if mime_type is None:
565             mime_type = 'application/octet-stream'
566         http_reply_and_exit(data, mime_type)
567     http_reply_and_exit("Document '%s' inconnu." % path_info, 'text/plain')
568
569 http_reply_and_exit("Requête '%s' inconnue." % path_info, 'text/plain')