[IMP] Use 'open' action in kanban views
[odoo/odoo.git] / addons / anonymization / anonymization.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU General Public License for more details.
17 #
18 #    You should have received a copy of the GNU General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 from lxml import etree
24 import os
25 import base64
26 try:
27     import cPickle as pickle
28 except ImportError:
29     import pickle
30 import random
31 import datetime
32 from osv import fields, osv
33 from tools.translate import _
34
35
36 FIELD_STATES = [('clear', 'Clear'), ('anonymized', 'Anonymized'), ('not_existing', 'Not Existing')]
37 ANONYMIZATION_STATES = FIELD_STATES + [('unstable', 'Unstable')]
38 ANONYMIZATION_HISTORY_STATE = [('started', 'Started'), ('done', 'Done'), ('in_exception', 'Exception occured')]
39 ANONYMIZATION_DIRECTION = [('clear -> anonymized', 'clear -> anonymized'), ('anonymized -> clear', 'anonymized -> clear')]
40
41
42 class ir_model_fields_anonymization(osv.osv):
43     _name = 'ir.model.fields.anonymization'
44     _rec_name = 'field_id'
45
46     _columns = {
47         'model_name': fields.char('Object Name', size=128, required=True),
48         'model_id': fields.many2one('ir.model', 'Object', ondelete='set null'),
49         'field_name': fields.char('Field Name', size=128, required=True),
50         'field_id': fields.many2one('ir.model.fields', 'Field', ondelete='set null'),
51         'state': fields.selection(selection=FIELD_STATES, String='Status', required=True, readonly=True),
52     }
53
54     _sql_constraints = [
55         ('model_id_field_id_uniq', 'unique (model_name, field_name)', _("You cannot have two fields with the same name on the same object!")),
56     ]
57
58     def _get_global_state(self, cr, uid, context=None):
59         ids = self.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
60         fields = self.browse(cr, uid, ids, context=context)
61         if not len(fields) or len(fields) == len([f for f in fields if f.state == 'clear']):
62             state = 'clear' # all fields are clear
63         elif len(fields) == len([f for f in fields if f.state == 'anonymized']):
64             state = 'anonymized' # all fields are anonymized
65         else:
66             state = 'unstable' # fields are mixed: this should be fixed
67         return state
68
69     def _check_write(self, cr, uid, context=None):
70         # check that the field is created from the menu and not from an database update
71         # otherwise the database update can crash:
72         if context.get('manual'):
73             global_state = self._get_global_state(cr, uid, context=context)
74             if global_state == 'anonymized':
75                 raise osv.except_osv('Error !', "The database is currently anonymized, you cannot create, modify or delete fields.")
76             elif global_state == 'unstable':
77                 msg = "The database anonymization is currently in an unstable state. Some fields are anonymized," + \
78                       " while some fields are not anonymized. You should try to solve this problem before trying to create, write or delete fields."
79                 raise osv.except_osv('Error !', msg)
80
81         return True
82
83     def _get_model_and_field_ids(self, cr, uid, vals, context=None):
84         model_and_field_ids = (False, False)
85
86         if 'field_name' in vals and vals['field_name'] and 'model_name' in vals and vals['model_name']:
87             ir_model_fields_obj = self.pool.get('ir.model.fields')
88             ir_model_obj = self.pool.get('ir.model')
89
90             model_ids = ir_model_obj.search(cr, uid, [('model', '=', vals['model_name'])], context=context)
91             if model_ids:
92                 field_ids = ir_model_fields_obj.search(cr, uid, [('name', '=', vals['field_name']), ('model_id', '=', model_ids[0])], context=context)
93                 if field_ids:
94                     field_id = field_ids[0]
95                     model_and_field_ids = (model_ids[0], field_id)
96
97         return model_and_field_ids
98
99     def create(self, cr, uid, vals, context=None):
100         # check field state: all should be clear before we can add a new field to anonymize:
101         self._check_write(cr, uid, context=context)
102
103         if 'field_name' in vals and vals['field_name'] and 'model_name' in vals and vals['model_name']:
104             vals['model_id'], vals['field_id'] = self._get_model_and_field_ids(cr, uid, vals, context=context)
105
106         # check not existing fields:
107         if not vals.get('field_id'):
108             vals['state'] = 'not_existing'
109
110         res = super(ir_model_fields_anonymization, self).create(cr, uid, vals, context=context)
111
112         return res
113
114     def write(self, cr, uid, ids, vals, context=None):
115         # check field state: all should be clear before we can modify a field:
116         if not (len(vals.keys()) == 1 and vals.get('state') == 'clear'):
117             self._check_write(cr, uid, context=context)
118
119         if 'field_name' in vals and vals['field_name'] and 'model_name' in vals and vals['model_name']:
120             vals['model_id'], vals['field_id'] = self._get_model_and_field_ids(cr, uid, vals, context=context)
121
122         # check not existing fields:
123         if 'field_id' in vals:
124             if not vals.get('field_id'):
125                 vals['state'] = 'not_existing'
126             else:
127                 global_state = self._get_global_state(cr, uid, context)
128                 if global_state != 'unstable':
129                     vals['state'] = global_state
130
131         res = super(ir_model_fields_anonymization, self).write(cr, uid, ids, vals, context=context)
132
133         return res
134
135     def unlink(self, cr, uid, ids, context=None):
136         # check field state: all should be clear before we can unlink a field:
137         self._check_write(cr, uid, context=context)
138
139         res = super(ir_model_fields_anonymization, self).unlink(cr, uid, ids, context=context)
140         return res
141
142     def onchange_model_id(self, cr, uid, ids, model_id, context=None):
143         res = {'value': {
144                     'field_name': False,
145                     'field_id': False,
146                     'model_name': False,
147               }}
148
149         if model_id:
150             ir_model_obj = self.pool.get('ir.model')
151             model_ids = ir_model_obj.search(cr, uid, [('id', '=', model_id)])
152             model_id = model_ids and model_ids[0] or None
153             model_name = model_id and ir_model_obj.browse(cr, uid, model_id).model or False
154             res['value']['model_name'] = model_name
155
156         return res
157
158     def onchange_model_name(self, cr, uid, ids, model_name, context=None):
159         res = {'value': {
160                     'field_name': False,
161                     'field_id': False,
162                     'model_id': False,
163               }}
164
165         if model_name:
166             ir_model_obj = self.pool.get('ir.model')
167             model_ids = ir_model_obj.search(cr, uid, [('model', '=', model_name)])
168             model_id = model_ids and model_ids[0] or False
169             res['value']['model_id'] = model_id
170
171         return res
172
173     def onchange_field_name(self, cr, uid, ids, field_name, model_name):
174         res = {'value': {
175                 'field_id': False,
176             }}
177
178         if field_name and model_name:
179             ir_model_fields_obj = self.pool.get('ir.model.fields')
180             field_ids = ir_model_fields_obj.search(cr, uid, [('name', '=', field_name), ('model', '=', model_name)])
181             field_id = field_ids and field_ids[0] or False
182             res['value']['field_id'] = field_id
183
184         return res
185
186     def onchange_field_id(self, cr, uid, ids, field_id, model_name):
187         res = {'value': {
188                     'field_name': False,
189               }}
190
191         if field_id:
192             ir_model_fields_obj = self.pool.get('ir.model.fields')
193             field = ir_model_fields_obj.browse(cr, uid, field_id)
194             res['value']['field_name'] = field.name
195
196         return res
197
198     _defaults = {
199         'state': lambda *a: 'clear',
200     }
201
202 ir_model_fields_anonymization()
203
204
205 class ir_model_fields_anonymization_history(osv.osv):
206     _name = 'ir.model.fields.anonymization.history'
207     _order = "date desc"
208
209     _columns = {
210         'date': fields.datetime('Date', required=True, readonly=True),
211         'field_ids': fields.many2many('ir.model.fields.anonymization', 'anonymized_field_to_history_rel', 'field_id', 'history_id', 'Fields', readonly=True),
212         'state': fields.selection(selection=ANONYMIZATION_HISTORY_STATE, string='Status', required=True, readonly=True),
213         'direction': fields.selection(selection=ANONYMIZATION_DIRECTION, string='Direction', required=True, readonly=True),
214         'msg': fields.text('Message', readonly=True),
215         'filepath': fields.char(string='File path', size=256, readonly=True),
216     }
217
218 ir_model_fields_anonymization_history()
219
220
221 class ir_model_fields_anonymize_wizard(osv.osv_memory):
222     _name = 'ir.model.fields.anonymize.wizard'
223
224     def _get_state(self, cr, uid, ids, name, arg, context=None):
225         res = {}
226
227         state = self._get_state_value(cr, uid, context=None)
228         for id in ids:
229             res[id] = state
230
231         return res
232
233     def _get_summary(self, cr, uid, ids, name, arg, context=None):
234         res = {}
235         summary = self._get_summary_value(cr, uid, context)
236         for id in ids:
237             res[id] = summary
238
239         return res
240
241     _columns = {
242         'name': fields.char(size=64, string='File Name'),
243         'summary': fields.function(_get_summary, type='text', string='Summary'),
244         'file_export': fields.binary(string='Export'),
245         'file_import': fields.binary(string='Import'),
246         'state': fields.function(_get_state, string='Status', type='selection', selection=ANONYMIZATION_STATES, readonly=False),
247         'msg': fields.text(string='Message'),
248     }
249
250     def _get_state_value(self, cr, uid, context=None):
251         state = self.pool.get('ir.model.fields.anonymization')._get_global_state(cr, uid, context=context)
252         return state
253
254     def _get_summary_value(self, cr, uid, context=None):
255         summary = u''
256         anon_field_obj = self.pool.get('ir.model.fields.anonymization')
257         ir_model_fields_obj = self.pool.get('ir.model.fields')
258
259         anon_field_ids = anon_field_obj.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
260         anon_fields = anon_field_obj.browse(cr, uid, anon_field_ids, context=context)
261
262         field_ids = [anon_field.field_id.id for anon_field in anon_fields if anon_field.field_id]
263         fields = ir_model_fields_obj.browse(cr, uid, field_ids, context=context)
264
265         fields_by_id = dict([(f.id, f) for f in fields])
266
267         for anon_field in anon_fields:
268             field = fields_by_id.get(anon_field.field_id.id)
269
270             values = {
271                 'model_name': field.model_id.name,
272                 'model_code': field.model_id.model,
273                 'field_code': field.name,
274                 'field_name': field.field_description,
275                 'state': anon_field.state,
276             }
277             summary += u" * %(model_name)s (%(model_code)s) -> %(field_name)s (%(field_code)s): state: (%(state)s)\n" % values
278
279         return summary
280
281     def default_get(self, cr, uid, fields_list, context=None):
282         res = {}
283         res['name'] = '.pickle'
284         res['summary'] = self._get_summary_value(cr, uid, context)
285         res['state'] = self._get_state_value(cr, uid, context)
286         res['msg'] = """Before executing the anonymization process, you should make a backup of your database."""
287
288         return res
289
290     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, *args, **kwargs):
291         state = self.pool.get('ir.model.fields.anonymization')._get_global_state(cr, uid, context=context)
292         
293         if context is None:
294             context = {}
295         
296         step = context.get('step', 'new_window')
297
298         res = super(ir_model_fields_anonymize_wizard, self).fields_view_get(cr, uid, view_id, view_type, context, *args, **kwargs)
299
300         eview = etree.fromstring(res['arch'])
301         placeholder = eview.xpath("group[@name='placeholder1']")
302         if len(placeholder):
303             placeholder = placeholder[0]
304             if step == 'new_window' and state == 'clear':
305                 # clicked in the menu and the fields are not anonymized: warn the admin that backuping the db is very important
306                 placeholder.addnext(etree.Element('field', {'name': 'msg', 'colspan': '4', 'nolabel': '1'}))
307                 placeholder.addnext(etree.Element('newline'))
308                 placeholder.addnext(etree.Element('label', {'string': 'Warning'}))
309                 eview.remove(placeholder)
310             elif step == 'new_window' and state == 'anonymized':
311                 # clicked in the menu and the fields are already anonymized
312                 placeholder.addnext(etree.Element('newline'))
313                 placeholder.addnext(etree.Element('field', {'name': 'file_import', 'required': "1"}))
314                 eview.remove(placeholder)
315             elif step == 'just_anonymized':
316                 # we just ran the anonymization process, we need the file export field
317                 placeholder.addnext(etree.Element('newline'))
318                 placeholder.addnext(etree.Element('field', {'name': 'file_export'}))
319                 # we need to remove the button:
320                 buttons = eview.xpath("button")
321                 for button in buttons:
322                     eview.remove(button)
323                 # and add a message:
324                 placeholder.addnext(etree.Element('field', {'name': 'msg', 'colspan': '4', 'nolabel': '1'}))
325                 placeholder.addnext(etree.Element('newline'))
326                 placeholder.addnext(etree.Element('label', {'string': 'Result'}))
327                 # remove the placeholer:
328                 eview.remove(placeholder)
329             elif step == 'just_desanonymized':
330                 # we just reversed the anonymization process, we don't need any field
331                 # we need to remove the button
332                 buttons = eview.xpath("button")
333                 for button in buttons:
334                     eview.remove(button)
335                 # and add a message
336                 # and add a message:
337                 placeholder.addnext(etree.Element('field', {'name': 'msg', 'colspan': '4', 'nolabel': '1'}))
338                 placeholder.addnext(etree.Element('newline'))
339                 placeholder.addnext(etree.Element('label', {'string': 'Result'}))
340                 # remove the placeholer:
341                 eview.remove(placeholder)
342             else:
343                 # unstable ?
344                 raise
345
346             res['arch'] = etree.tostring(eview)
347
348         return res
349
350     def _raise_after_history_update(self, cr, uid, history_id, error_type, error_msg):
351         self.pool.get('ir.model.fields.anonymization.history').write(cr, uid, history_id, {
352             'state': 'in_exception',
353             'msg': error_msg,
354         })
355         raise osv.except_osv(error_type, error_msg)
356
357     def anonymize_database(self,cr, uid, ids, context=None):
358         """Sets the 'anonymized' state to defined fields"""
359
360         # create a new history record:
361         anonymization_history_model = self.pool.get('ir.model.fields.anonymization.history')
362
363         vals = {
364             'date': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
365             'state': 'started',
366             'direction': 'clear -> anonymized',
367         }
368         history_id = anonymization_history_model.create(cr, uid, vals)
369
370         # check that all the defined fields are in the 'clear' state
371         state = self.pool.get('ir.model.fields.anonymization')._get_global_state(cr, uid, context=context)
372         if state == 'anonymized':
373             self._raise_after_history_update(cr, uid, history_id, 'Error !', "The database is currently anonymized, you cannot anonymize it again.")
374         elif state == 'unstable':
375             msg = "The database anonymization is currently in an unstable state. Some fields are anonymized," + \
376                   " while some fields are not anonymized. You should try to solve this problem before trying to do anything."
377             self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
378
379         # do the anonymization:
380         dirpath = os.environ.get('HOME') or os.getcwd()
381         rel_filepath = 'field_anonymization_%s_%s.pickle' % (cr.dbname, history_id)
382         abs_filepath = os.path.abspath(os.path.join(dirpath, rel_filepath))
383
384         ir_model_fields_anonymization_model = self.pool.get('ir.model.fields.anonymization')
385         field_ids = ir_model_fields_anonymization_model.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
386         fields = ir_model_fields_anonymization_model.browse(cr, uid, field_ids, context=context)
387
388         if not fields:
389             msg = "No fields are going to be anonymized."
390             self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
391
392         data = []
393
394         for field in fields:
395             model_name = field.model_id.model
396             field_name = field.field_id.name
397             field_type = field.field_id.ttype
398             table_name = self.pool.get(model_name)._table
399
400             # get the current value
401             sql = "select id, %s from %s" % (field_name, table_name)
402             cr.execute(sql)
403             records = cr.dictfetchall()
404             for record in records:
405                 data.append({"model_id": model_name, "field_id": field_name, "id": record['id'], "value": record[field_name]})
406
407                 # anonymize the value:
408                 anonymized_value = None
409
410                 sid = str(record['id'])
411                 if field_type == 'char':
412                     anonymized_value = 'xxx'+sid
413                 elif field_type == 'selection':
414                     anonymized_value = 'xxx'+sid
415                 elif field_type == 'text':
416                     anonymized_value = 'xxx'+sid
417                 elif field_type == 'boolean':
418                     anonymized_value = random.choice([True, False])
419                 elif field_type == 'date':
420                     anonymized_value = '2011-11-11'
421                 elif field_type == 'datetime':
422                     anonymized_value = '2011-11-11 11:11:11'
423                 elif field_type == 'float':
424                     anonymized_value = 0.0
425                 elif field_type == 'integer':
426                     anonymized_value = 0
427                 elif field_type in ['binary', 'many2many', 'many2one', 'one2many', 'reference']: # cannot anonymize these kind of fields
428                     msg = "Cannot anonymize fields of these types: binary, many2many, many2one, one2many, reference."
429                     self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
430
431                 if anonymized_value is None:
432                     self._raise_after_history_update(cr, uid, history_id, 'Error !', "Anonymized value is None. This cannot happens.")
433
434                 sql = "update %(table)s set %(field)s = %%(anonymized_value)s where id = %%(id)s" % {
435                     'table': table_name,
436                     'field': field_name,
437                 }
438                 cr.execute(sql, {
439                     'anonymized_value': anonymized_value,
440                     'id': record['id']
441                 })
442
443         # save pickle:
444         fn = open(abs_filepath, 'w')
445         pickle.dump(data, fn, pickle.HIGHEST_PROTOCOL)
446
447         # update the anonymization fields:
448         values = {
449             'state': 'anonymized',
450         }
451         ir_model_fields_anonymization_model.write(cr, uid, field_ids, values, context=context)
452
453         # add a result message in the wizard:
454         msgs = ["Anonymization successful.",
455                "",
456                "Donot forget to save the resulting file to a safe place because you will not be able to revert the anonymization without this file.",
457                "",
458                "This file is also stored in the %s directory. The absolute file path is: %s.",
459               ]
460         msg = '\n'.join(msgs) % (dirpath, abs_filepath)
461
462         fn = open(abs_filepath, 'r')
463
464         self.write(cr, uid, ids, {
465             'msg': msg,
466             'file_export': base64.encodestring(fn.read()),
467         })
468         fn.close()
469
470         # update the history record:
471         anonymization_history_model.write(cr, uid, history_id, {
472             'field_ids': [[6, 0, field_ids]],
473             'msg': msg,
474             'filepath': abs_filepath,
475             'state': 'done',
476         })
477
478         # handle the view:
479         view_id = self._id_get(cr, uid, 'ir.ui.view', 'view_ir_model_fields_anonymize_wizard_form', 'anonymization')
480
481         return {
482                 'res_id': ids[0],
483                 'view_id': [view_id],
484                 'view_type': 'form',
485                 "view_mode": 'form',
486                 'res_model': 'ir.model.fields.anonymize.wizard',
487                 'type': 'ir.actions.act_window',
488                 'context': {'step': 'just_anonymized'},
489                 'target':'new',
490         }
491
492     def reverse_anonymize_database(self,cr, uid, ids, context=None):
493         """Set the 'clear' state to defined fields"""
494
495         ir_model_fields_anonymization_model = self.pool.get('ir.model.fields.anonymization')
496         anonymization_history_model = self.pool.get('ir.model.fields.anonymization.history')
497
498         # create a new history record:
499         vals = {
500             'date': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
501             'state': 'started',
502             'direction': 'anonymized -> clear',
503         }
504         history_id = anonymization_history_model.create(cr, uid, vals)
505
506         # check that all the defined fields are in the 'anonymized' state
507         state = ir_model_fields_anonymization_model._get_global_state(cr, uid, context=context)
508         if state == 'clear':
509             raise osv.except_osv('Error !', "The database is not currently anonymized, you cannot reverse the anonymization.")
510         elif state == 'unstable':
511             msg = "The database anonymization is currently in an unstable state. Some fields are anonymized," + \
512                   " while some fields are not anonymized. You should try to solve this problem before trying to do anything."
513             raise osv.except_osv('Error !', msg)
514
515         wizards = self.browse(cr, uid, ids, context=context)
516         for wizard in wizards:
517             if not wizard.file_import:
518                 msg = "It is not possible to reverse the anonymization process without supplying anonymization export file."
519                 self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
520
521             # reverse the anonymization:
522             # load the pickle file content into a data structure:
523             data = pickle.loads(base64.decodestring(wizard.file_import))
524
525             for line in data:
526                 table_name = self.pool.get(line['model_id'])._table
527                 sql = "update %(table)s set %(field)s = %%(value)s where id = %%(id)s" % {
528                     'table': table_name,
529                     'field': line['field_id'],
530                 }
531                 cr.execute(sql, {
532                     'value': line['value'],
533                     'id': line['id']
534                 })
535
536             # update the anonymization fields:
537             ir_model_fields_anonymization_model = self.pool.get('ir.model.fields.anonymization')
538             field_ids = ir_model_fields_anonymization_model.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
539             values = {
540                 'state': 'clear',
541             }
542             ir_model_fields_anonymization_model.write(cr, uid, field_ids, values, context=context)
543
544             # add a result message in the wizard:
545             msg = '\n'.join(["Successfully reversed the anonymization.",
546                              "",
547                             ])
548
549             self.write(cr, uid, ids, {'msg': msg})
550
551             # update the history record:
552             anonymization_history_model.write(cr, uid, history_id, {
553                 'field_ids': [[6, 0, field_ids]],
554                 'msg': msg,
555                 'filepath': False,
556                 'state': 'done',
557             })
558
559             # handle the view:
560             view_id = self._id_get(cr, uid, 'ir.ui.view', 'view_ir_model_fields_anonymize_wizard_form', 'anonymization')
561
562             return {
563                     'res_id': ids[0],
564                     'view_id': [view_id],
565                     'view_type': 'form',
566                     "view_mode": 'form',
567                     'res_model': 'ir.model.fields.anonymize.wizard',
568                     'type': 'ir.actions.act_window',
569                     'context': {'step': 'just_desanonymized'},
570                     'target':'new',
571             }
572
573     def _id_get(self, cr, uid, model, id_str, mod):
574         if '.' in id_str:
575             mod, id_str = id_str.split('.')
576         try:
577             idn = self.pool.get('ir.model.data')._get_id(cr, uid, mod, id_str)
578             res = int(self.pool.get('ir.model.data').read(cr, uid, [idn], ['res_id'])[0]['res_id'])
579         except:
580             res = None
581         return res
582
583 ir_model_fields_anonymize_wizard()
584
585
586 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: