wcs/list2form : filtrage des lignes vides
[progfou.git] / wcs / wcs-extract
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """
4 Outil d'export 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 : 15 octobre 2009
10
11 Depends: wcs, python-simplejson, python-magic
12 """
13 import os
14 import os.path
15 import shutil
16 import logging
17 from time import gmtime, strftime, struct_time
18 import simplejson as json
19 import magic
20 import mimetypes
21 import unicodedata
22
23 from wcs import publisher
24 from wcs.formdef import FormDef
25 from wcs.fields import TitleField, CommentField, TextField, \
26                        StringField, ItemField, ItemsField, EmailField, \
27                        DateField, FileField, BoolField, TableField
28
29
30 def reduce_to_alnum(s, replacement_char='-'):
31     """réduction d'une chaîne de caractères à de l'alpha-numérique"""
32
33     if type(s) is not unicode:
34         s = unicode(s, 'utf-8')
35     s = unicodedata.normalize('NFKD', s).encode('ASCII', 'ignore')
36     r = ''
37     for c in s:
38         if ('a' <= c.lower() <= 'z') or ('0' <= c <= '9'):
39             r += c
40         elif len(r) > 0 and r[-1] != replacement_char:
41             r += replacement_char
42         else: # r == '' or r[-1] == replacement_char
43             pass
44     return r.strip(replacement_char)
45
46
47 def extract_fields(formdef, output_directory):
48     """nommage des champs de façon unique"""
49     # TODO: devrait retourner un résultat, qui serait alors sauvé en dehors
50
51     # XXX: hack temporaire… :-/
52     global field_names
53
54     f = open(os.path.join(output_directory, 'field-names.txt'), 'w')
55
56     field_names = {}
57     field_names_duplicates = {}
58     for field in formdef.fields:
59         if isinstance(field, TitleField) or isinstance(field, CommentField):
60             continue
61         if field.varname:
62             name = field.varname
63         else:
64             name = reduce_to_alnum(field.label,'_').lower()
65         if name in field_names.values(): # duplicat
66             field_names_duplicates[name] = field_names_duplicates.get(name, 1) + 1
67             name = '%s_%d' % (name, field_names_duplicates[name])
68         field_names.update({field.id: name})
69         print >>f, "%s:%s:%s" % (field.id, field_names[field.id], field.label)
70
71     f.close()
72
73     f = open(os.path.join(output_directory, 'field-names.json'), 'wb')
74     f.write(json.dumps(field_names, ensure_ascii=False))
75     f.close()
76
77
78 def extract_data(formdef, output_directory):
79     """extraction des données du formulaire"""
80     # TODO: devrait retourner un résultat, qui serait alors sauvé en dehors
81
82     # XXX: hack temporaire… :-/
83     global pub
84
85     # on charge la base des types MIME une fois pour toutes
86     #magicmime = magic.Magic(mime=True) => ce sera pour plus tard…
87     magicmime = magic.open(magic.MAGIC_MIME)
88     magicmime.load()
89
90     liste_dossiers = []
91     for object in formdef.data_class().select():
92         if object.user is None:
93             logging.warning("Dossier '%s' sans utilisateur associé ?!?"\
94                             " On ignore...", object.id)
95             continue
96
97         result = {
98             'num_dossier': object.id,
99             'wcs_status': object.status,
100             'wcs_workflow_status': (object.status.startswith('wf-') and \
101                                 object.get_workflow_status().name or None),
102             'wcs_user_email': object.user.email,
103             'wcs_user_display_name': object.user.display_name,
104            #'wcs_last_modified': strftime('%Y-%m-%d %H:%M:%S', gmtime(object.last_modified())),
105             'wcs_comments': [],
106         }
107
108         if object.evolution is not None:
109             for e in object.evolution:
110                 if e.comment is not None:
111                     who = pub.user_class.get(e.who).display_name
112                     time = strftime('%Y-%m-%d %H:%M:%S', e.time)
113                     comment = '%s -- %s %s' % (e.comment, who, time)
114                     result['wcs_comments'].append(comment)
115
116         qfiles = { }
117         for field in formdef.fields:
118             field_id = str(field.id)
119             if not field_id in object.data:
120                 continue
121             if isinstance(field, TitleField) or isinstance(field, CommentField):
122                 continue
123             field_name = field_names[field_id]
124             data = object.data.get(field_id)
125             if data is None:
126                 result[field_name] = None
127                 continue
128             if isinstance(field, StringField) or isinstance(field, TextField) \
129             or isinstance(field, EmailField) or isinstance(field, ItemField):
130                 result[field_name] = data
131             elif isinstance(field, ItemsField) or isinstance(field, TableField):
132                 result[field_name] = data # liste => peux-être joindre sur ';'
133             elif isinstance(field, BoolField):
134                 result[field_name] = (data == 'True')
135             elif isinstance(field, DateField):
136                 if isinstance(data, struct_time):
137                     result[field_name] = '%04d-%02d-%02d' % (data.tm_year,
138                                                     data.tm_mon, data.tm_mday)
139                 else:
140                     result[field_name] = data
141             elif isinstance(field, FileField):
142                 if '.' in data.orig_filename:
143                     extension = data.orig_filename.rpartition('.')[2].lower()
144                 else: # il n'y a pas d'extension dans le nom de fichier
145                     p = os.path.join(pub.app_dir, 'uploads', data.qfilename)
146                     try:
147                         #m = magicmime.from_file(p) => ce sera pour plus tard…
148                         m = magicmime.file(p).split()[0].strip(';')
149                         extension = mimetypes.guess_extension(m)
150                     except:
151                         logging.warning("Type de fichier inconnu pour '%s'.", p)
152                         extension = None
153                     if extension is not None:
154                         extension = extension[1:]
155                     else:
156                         extension = 'unknown'
157                 result[field_name] = "%s.%s" % (field_name, extension)
158                 qfiles[field_name] = data.qfilename
159             else:
160                 logging.warning("Type de champ inconnu '%s' pour '%s' (%s).",
161                             field.__class__.__name__, field_name, field.label)
162
163         num_dossier = result['num_dossier']
164         nom = reduce_to_alnum(result.get('nom','sans-nom')).upper()
165         prenom = reduce_to_alnum(result.get('prenom','sans-prenom')).upper()
166         adel = result.get('adresse_electronique','sans-adel').replace('@','-').lower()
167
168         filename = "%04d-%s-%s-%s" % (num_dossier, nom, prenom, adel)
169         liste_dossiers.append(filename + '.json')
170
171         # copie des fichiers joints
172         for f in qfiles:
173             result[f] = filename + '_' + result[f]
174             src = os.path.join(pub.app_dir, 'uploads', qfiles[f])
175             dst = os.path.join(output_directory, 'data', result[f])
176             if not os.path.exists(dst) or os.path.getmtime(src) > os.path.getmtime(dst):
177                 shutil.copy2(src, dst)
178                 os.chmod(dst, 0644)
179
180         # génération du fichier JSON
181         jsonname = os.path.join(output_directory, 'data', filename + '.json')
182         f = open(jsonname, 'wb')
183         f.write(json.dumps(result, ensure_ascii=False).encode('utf-8'))
184         f.close()
185
186         logging.info("Dossier '%s' : %s.",
187                                     filename, result['wcs_workflow_status'])
188
189     liste_dossiers.sort()
190     f = open(os.path.join(output_directory, 'liste-dossiers.json'), 'wb')
191     f.write(json.dumps(liste_dossiers, ensure_ascii=False))
192     f.close()
193
194
195 if __name__ == '__main__':
196     import sys
197
198     if len(sys.argv) != 4:
199         print >>sys.stderr, "Usage : %s <dossier-destination> <site> <formulaire>" % sys.argv[0]
200         sys.exit(1)
201
202     VHOST = sys.argv[2]
203     FORM_NAME = sys.argv[3]
204     OUTPUT_DIRECTORY = os.path.join(sys.argv[1], VHOST, FORM_NAME)
205
206     os.umask(0022)
207     # création du dossier d'extraction, au besoin
208     if not os.path.isdir(os.path.join(OUTPUT_DIRECTORY, 'data')):
209         os.makedirs(os.path.join(OUTPUT_DIRECTORY, 'data'), 0755)
210
211     logging.basicConfig(level=logging.DEBUG,
212         format='%(asctime)s %(levelname)s %(message)s',
213         filename=os.path.join(OUTPUT_DIRECTORY, 'last-run.log'),
214         filemode='w')
215
216     logging.info('Début.')
217
218     pub = publisher.WcsPublisher.create_publisher()
219     pub.app_dir = os.path.join(pub.app_dir, VHOST)
220
221     formdef = FormDef.get_by_urlname(FORM_NAME)
222
223     extract_fields(formdef, OUTPUT_DIRECTORY)
224
225     try:
226         extract_data(formdef, OUTPUT_DIRECTORY)
227     except:
228         logging.exception("Interruption du traitement pour cause d'erreur !")
229
230     logging.info('Fin.')
231