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