5276b42952cceb9d38b1f2317253a2fbdd8cf650
[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         try:
98             workflow_status = object.status.startswith('wf-') and \
99                             object.get_workflow_status().name or None
100         except:
101             workflow_status = None
102
103         result = {
104             'num_dossier': object.id,
105             'wcs_status': object.status,
106             'wcs_workflow_status': workflow_status,
107             'wcs_user_email': object.user.email,
108             'wcs_user_display_name': object.user.display_name,
109            #'wcs_last_modified': strftime('%Y-%m-%d %H:%M:%S', gmtime(object.last_modified())),
110             'wcs_comments': [],
111         }
112
113         if object.evolution is not None:
114             for e in object.evolution:
115                 if e.comment is not None:
116                     who = pub.user_class.get(e.who).display_name
117                     time = strftime('%Y-%m-%d %H:%M:%S', e.time)
118                     comment = '%s -- %s %s' % (e.comment, who, time)
119                     result['wcs_comments'].append(comment)
120
121         qfiles = { }
122         for field in formdef.fields:
123             field_id = str(field.id)
124             if not field_id in object.data:
125                 continue
126             if isinstance(field, TitleField) or isinstance(field, CommentField):
127                 continue
128             field_name = field_names[field_id]
129             data = object.data.get(field_id)
130             if data is None:
131                 result[field_name] = None
132                 continue
133             if isinstance(field, StringField) or isinstance(field, TextField) \
134             or isinstance(field, EmailField) or isinstance(field, ItemField):
135                 result[field_name] = data
136             elif isinstance(field, ItemsField) or isinstance(field, TableField):
137                 result[field_name] = data # liste => peux-être joindre sur ';'
138             elif isinstance(field, BoolField):
139                 result[field_name] = (data == 'True')
140             elif isinstance(field, DateField):
141                 if isinstance(data, struct_time):
142                     result[field_name] = '%04d-%02d-%02d' % (data.tm_year,
143                                                     data.tm_mon, data.tm_mday)
144                 else:
145                     result[field_name] = data
146             elif isinstance(field, FileField):
147                 if '.' in data.orig_filename:
148                     extension = data.orig_filename.rpartition('.')[2].lower()
149                 else: # il n'y a pas d'extension dans le nom de fichier
150                     p = os.path.join(pub.app_dir, 'uploads', data.qfilename)
151                     try:
152                         #m = magicmime.from_file(p) => ce sera pour plus tard…
153                         m = magicmime.file(p).split()[0].strip(';')
154                         extension = mimetypes.guess_extension(m)
155                     except:
156                         logging.warning("Type de fichier inconnu pour '%s'.", p)
157                         extension = None
158                     if extension is not None:
159                         extension = extension[1:]
160                     else:
161                         extension = 'unknown'
162                 result[field_name] = "%s.%s" % (field_name, extension)
163                 qfiles[field_name] = data.qfilename
164             else:
165                 logging.warning("Type de champ inconnu '%s' pour '%s' (%s).",
166                             field.__class__.__name__, field_name, field.label)
167
168         num_dossier = result['num_dossier']
169         nom = reduce_to_alnum(result.get('nom','sans-nom')).upper()
170         prenom = reduce_to_alnum(result.get('prenom','sans-prenom')).upper()
171         adel = result.get('adresse_electronique','sans-adel').replace('@','-').lower()
172
173         filename = "%04d-%s-%s-%s" % (num_dossier, nom, prenom, adel)
174         liste_dossiers.append(filename + '.json')
175
176         # création du sous-dossier destination, au besoin
177         dstdir = os.path.join(output_directory, 'data', result['wcs_status'])
178         if not os.path.isdir(dstdir):
179             os.mkdir(dstdir)
180
181         # copie des fichiers joints
182         for f in qfiles:
183             result[f] = filename + '_' + result[f]
184             src = os.path.join(pub.app_dir, 'uploads', qfiles[f])
185             dst = os.path.join(dstdir, result[f])
186             if not os.path.exists(dst) or os.path.getmtime(src) > os.path.getmtime(dst):
187                 shutil.copy2(src, dst)
188                 os.chmod(dst, 0644)
189
190         # génération du fichier JSON
191         jsonname = os.path.join(dstdir, filename + '.json')
192         f = open(jsonname, 'wb')
193         f.write(json.dumps(result, ensure_ascii=False).encode('utf-8'))
194         f.close()
195
196         logging.info("Dossier '%s' : %s.",
197                                     filename, result['wcs_workflow_status'])
198
199     liste_dossiers.sort()
200     f = open(os.path.join(output_directory, 'liste-dossiers.json'), 'wb')
201     f.write(json.dumps(liste_dossiers, ensure_ascii=False))
202     f.close()
203
204
205 if __name__ == '__main__':
206     import sys
207
208     if len(sys.argv) != 4:
209         print >>sys.stderr, "Usage : %s <dossier-destination> <site> <formulaire>" % sys.argv[0]
210         sys.exit(1)
211
212     VHOST = sys.argv[2]
213     FORM_NAME = sys.argv[3]
214     OUTPUT_DIRECTORY = os.path.join(sys.argv[1], VHOST, FORM_NAME)
215
216     os.umask(0022)
217     # création du dossier d'extraction, au besoin
218     if not os.path.isdir(os.path.join(OUTPUT_DIRECTORY, 'data')):
219         os.makedirs(os.path.join(OUTPUT_DIRECTORY, 'data'), 0755)
220
221     logging.basicConfig(level=logging.DEBUG,
222         format='%(asctime)s %(levelname)s %(message)s',
223         filename=os.path.join(OUTPUT_DIRECTORY, 'last-run.log'),
224         filemode='w')
225
226     logging.info('Début.')
227
228     pub = publisher.WcsPublisher.create_publisher()
229     pub.app_dir = os.path.join(pub.app_dir, VHOST)
230
231     formdef = FormDef.get_by_urlname(FORM_NAME)
232
233     extract_fields(formdef, OUTPUT_DIRECTORY)
234
235     try:
236         extract_data(formdef, OUTPUT_DIRECTORY)
237     except:
238         logging.exception("Interruption du traitement pour cause d'erreur !")
239
240     logging.info('Fin.')
241