1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
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.
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.
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/>.
21 ##############################################################################
23 from lxml import etree
27 import cPickle as pickle
32 from openerp.osv import fields, osv
33 from openerp.tools.translate import _
35 from itertools import groupby
36 from operator import itemgetter
39 FIELD_STATES = [('clear', 'Clear'), ('anonymized', 'Anonymized'), ('not_existing', 'Not Existing'), ('new', 'New')]
40 ANONYMIZATION_STATES = FIELD_STATES + [('unstable', 'Unstable')]
41 WIZARD_ANONYMIZATION_STATES = [('clear', 'Clear'), ('anonymized', 'Anonymized'), ('unstable', 'Unstable')]
42 ANONYMIZATION_HISTORY_STATE = [('started', 'Started'), ('done', 'Done'), ('in_exception', 'Exception occured')]
43 ANONYMIZATION_DIRECTION = [('clear -> anonymized', 'clear -> anonymized'), ('anonymized -> clear', 'anonymized -> clear')]
47 if isinstance(cols, basestring):
49 return dict((k, [v for v in itr]) for k, itr in groupby(sorted(lst, key=itemgetter(*cols)), itemgetter(*cols)))
52 class ir_model_fields_anonymization(osv.osv):
53 _name = 'ir.model.fields.anonymization'
54 _rec_name = 'field_id'
57 'model_name': fields.char('Object Name', size=128, required=True),
58 'model_id': fields.many2one('ir.model', 'Object', ondelete='set null'),
59 'field_name': fields.char('Field Name', size=128, required=True),
60 'field_id': fields.many2one('ir.model.fields', 'Field', ondelete='set null'),
61 'state': fields.selection(selection=FIELD_STATES, String='Status', required=True, readonly=True),
65 ('model_id_field_id_uniq', 'unique (model_name, field_name)', _("You cannot have two fields with the same name on the same object!")),
68 def _get_global_state(self, cr, uid, context=None):
69 ids = self.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
70 fields = self.browse(cr, uid, ids, context=context)
71 if not len(fields) or len(fields) == len([f for f in fields if f.state == 'clear']):
72 state = 'clear' # all fields are clear
73 elif len(fields) == len([f for f in fields if f.state == 'anonymized']):
74 state = 'anonymized' # all fields are anonymized
76 state = 'unstable' # fields are mixed: this should be fixed
80 def _check_write(self, cr, uid, context=None):
81 """check that the field is created from the menu and not from an database update
82 otherwise the database update can crash:"""
86 if context.get('manual'):
87 global_state = self._get_global_state(cr, uid, context=context)
88 if global_state == 'anonymized':
89 raise osv.except_osv('Error!', "The database is currently anonymized, you cannot create, modify or delete fields.")
90 elif global_state == 'unstable':
91 msg = _("The database anonymization is currently in an unstable state. Some fields are anonymized," + \
92 " while some fields are not anonymized. You should try to solve this problem before trying to create, write or delete fields.")
93 raise osv.except_osv('Error!', msg)
97 def _get_model_and_field_ids(self, cr, uid, vals, context=None):
98 model_and_field_ids = (False, False)
100 if 'field_name' in vals and vals['field_name'] and 'model_name' in vals and vals['model_name']:
101 ir_model_fields_obj = self.pool.get('ir.model.fields')
102 ir_model_obj = self.pool.get('ir.model')
104 model_ids = ir_model_obj.search(cr, uid, [('model', '=', vals['model_name'])], context=context)
106 field_ids = ir_model_fields_obj.search(cr, uid, [('name', '=', vals['field_name']), ('model_id', '=', model_ids[0])], context=context)
108 field_id = field_ids[0]
109 model_and_field_ids = (model_ids[0], field_id)
111 return model_and_field_ids
113 def create(self, cr, uid, vals, context=None):
114 # check field state: all should be clear before we can add a new field to anonymize:
115 self._check_write(cr, uid, context=context)
117 global_state = self._get_global_state(cr, uid, context=context)
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)
122 # check not existing fields:
123 if not vals.get('field_id'):
124 vals['state'] = 'not_existing'
126 vals['state'] = global_state
128 res = super(ir_model_fields_anonymization, self).create(cr, uid, vals, context=context)
132 def write(self, cr, uid, ids, vals, context=None):
133 # check field state: all should be clear before we can modify a field:
134 if not (len(vals.keys()) == 1 and vals.get('state') == 'clear'):
135 self._check_write(cr, uid, context=context)
137 if 'field_name' in vals and vals['field_name'] and 'model_name' in vals and vals['model_name']:
138 vals['model_id'], vals['field_id'] = self._get_model_and_field_ids(cr, uid, vals, context=context)
140 # check not existing fields:
141 if 'field_id' in vals:
142 if not vals.get('field_id'):
143 vals['state'] = 'not_existing'
145 global_state = self._get_global_state(cr, uid, context)
146 if global_state != 'unstable':
147 vals['state'] = global_state
149 res = super(ir_model_fields_anonymization, self).write(cr, uid, ids, vals, context=context)
153 def unlink(self, cr, uid, ids, context=None):
154 # check field state: all should be clear before we can unlink a field:
155 self._check_write(cr, uid, context=context)
157 res = super(ir_model_fields_anonymization, self).unlink(cr, uid, ids, context=context)
160 def onchange_model_id(self, cr, uid, ids, model_id, context=None):
168 ir_model_obj = self.pool.get('ir.model')
169 model_ids = ir_model_obj.search(cr, uid, [('id', '=', model_id)])
170 model_id = model_ids and model_ids[0] or None
171 model_name = model_id and ir_model_obj.browse(cr, uid, model_id).model or False
172 res['value']['model_name'] = model_name
176 def onchange_model_name(self, cr, uid, ids, model_name, context=None):
184 ir_model_obj = self.pool.get('ir.model')
185 model_ids = ir_model_obj.search(cr, uid, [('model', '=', model_name)])
186 model_id = model_ids and model_ids[0] or False
187 res['value']['model_id'] = model_id
191 def onchange_field_name(self, cr, uid, ids, field_name, model_name):
196 if field_name and model_name:
197 ir_model_fields_obj = self.pool.get('ir.model.fields')
198 field_ids = ir_model_fields_obj.search(cr, uid, [('name', '=', field_name), ('model', '=', model_name)])
199 field_id = field_ids and field_ids[0] or False
200 res['value']['field_id'] = field_id
204 def onchange_field_id(self, cr, uid, ids, field_id, model_name):
210 ir_model_fields_obj = self.pool.get('ir.model.fields')
211 field = ir_model_fields_obj.browse(cr, uid, field_id)
212 res['value']['field_name'] = field.name
217 'state': lambda *a: 'clear',
221 class ir_model_fields_anonymization_history(osv.osv):
222 _name = 'ir.model.fields.anonymization.history'
226 'date': fields.datetime('Date', required=True, readonly=True),
227 'field_ids': fields.many2many('ir.model.fields.anonymization', 'anonymized_field_to_history_rel', 'field_id', 'history_id', 'Fields', readonly=True),
228 'state': fields.selection(selection=ANONYMIZATION_HISTORY_STATE, string='Status', required=True, readonly=True),
229 'direction': fields.selection(selection=ANONYMIZATION_DIRECTION, string='Direction', required=True, readonly=True),
230 'msg': fields.text('Message', readonly=True),
231 'filepath': fields.char(string='File path', size=256, readonly=True),
235 class ir_model_fields_anonymize_wizard(osv.osv_memory):
236 _name = 'ir.model.fields.anonymize.wizard'
238 def _get_state(self, cr, uid, ids, name, arg, context=None):
241 state = self._get_state_value(cr, uid, context=None)
247 def _get_summary(self, cr, uid, ids, name, arg, context=None):
249 summary = self._get_summary_value(cr, uid, context)
256 'name': fields.char(size=64, string='File Name'),
257 'summary': fields.function(_get_summary, type='text', string='Summary'),
258 'file_export': fields.binary(string='Export'),
259 'file_import': fields.binary(string='Import', help="This is the file created by the anonymization process. It should have the '.pickle' extention."),
260 'state': fields.function(_get_state, string='Status', type='selection', selection=WIZARD_ANONYMIZATION_STATES, readonly=False),
261 'msg': fields.text(string='Message'),
264 def _get_state_value(self, cr, uid, context=None):
265 state = self.pool.get('ir.model.fields.anonymization')._get_global_state(cr, uid, context=context)
268 def _get_summary_value(self, cr, uid, context=None):
270 anon_field_obj = self.pool.get('ir.model.fields.anonymization')
271 ir_model_fields_obj = self.pool.get('ir.model.fields')
273 anon_field_ids = anon_field_obj.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
274 anon_fields = anon_field_obj.browse(cr, uid, anon_field_ids, context=context)
276 field_ids = [anon_field.field_id.id for anon_field in anon_fields if anon_field.field_id]
277 fields = ir_model_fields_obj.browse(cr, uid, field_ids, context=context)
279 fields_by_id = dict([(f.id, f) for f in fields])
281 for anon_field in anon_fields:
282 field = fields_by_id.get(anon_field.field_id.id)
285 'model_name': field.model_id.name,
286 'model_code': field.model_id.model,
287 'field_code': field.name,
288 'field_name': field.field_description,
289 'state': anon_field.state,
291 summary += u" * %(model_name)s (%(model_code)s) -> %(field_name)s (%(field_code)s): state: (%(state)s)\n" % values
295 def default_get(self, cr, uid, fields_list, context=None):
297 res['name'] = '.pickle'
298 res['summary'] = self._get_summary_value(cr, uid, context)
299 res['state'] = self._get_state_value(cr, uid, context)
300 res['msg'] = _("""Before executing the anonymization process, you should make a backup of your database.""")
304 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, *args, **kwargs):
305 state = self.pool.get('ir.model.fields.anonymization')._get_global_state(cr, uid, context=context)
310 step = context.get('step', 'new_window')
312 res = super(ir_model_fields_anonymize_wizard, self).fields_view_get(cr, uid, view_id, view_type, context, *args, **kwargs)
314 eview = etree.fromstring(res['arch'])
315 placeholder = eview.xpath("group[@name='placeholder1']")
317 placeholder = placeholder[0]
318 if step == 'new_window' and state == 'clear':
319 # clicked in the menu and the fields are not anonymized: warn the admin that backuping the db is very important
320 placeholder.addnext(etree.Element('field', {'name': 'msg', 'colspan': '4', 'nolabel': '1'}))
321 placeholder.addnext(etree.Element('newline'))
322 placeholder.addnext(etree.Element('label', {'string': 'Warning'}))
323 eview.remove(placeholder)
324 elif step == 'new_window' and state == 'anonymized':
325 # clicked in the menu and the fields are already anonymized
326 placeholder.addnext(etree.Element('newline'))
327 placeholder.addnext(etree.Element('field', {'name': 'file_import', 'required': "1"}))
328 placeholder.addnext(etree.Element('label', {'string': 'Anonymization file'}))
329 eview.remove(placeholder)
330 elif step == 'just_anonymized':
331 # we just ran the anonymization process, we need the file export field
332 placeholder.addnext(etree.Element('newline'))
333 placeholder.addnext(etree.Element('field', {'name': 'file_export'}))
334 # we need to remove the button:
335 buttons = eview.xpath("button")
336 for button in buttons:
339 placeholder.addnext(etree.Element('field', {'name': 'msg', 'colspan': '4', 'nolabel': '1'}))
340 placeholder.addnext(etree.Element('newline'))
341 placeholder.addnext(etree.Element('label', {'string': 'Result'}))
342 # remove the placeholer:
343 eview.remove(placeholder)
344 elif step == 'just_desanonymized':
345 # we just reversed the anonymization process, we don't need any field
346 # we need to remove the button
347 buttons = eview.xpath("button")
348 for button in buttons:
352 placeholder.addnext(etree.Element('field', {'name': 'msg', 'colspan': '4', 'nolabel': '1'}))
353 placeholder.addnext(etree.Element('newline'))
354 placeholder.addnext(etree.Element('label', {'string': 'Result'}))
355 # remove the placeholer:
356 eview.remove(placeholder)
358 msg = _("The database anonymization is currently in an unstable state. Some fields are anonymized," + \
359 " while some fields are not anonymized. You should try to solve this problem before trying to do anything else.")
360 raise osv.except_osv('Error!', msg)
362 res['arch'] = etree.tostring(eview)
366 def _raise_after_history_update(self, cr, uid, history_id, error_type, error_msg):
367 self.pool.get('ir.model.fields.anonymization.history').write(cr, uid, history_id, {
368 'state': 'in_exception',
371 raise osv.except_osv(error_type, error_msg)
373 def anonymize_database(self, cr, uid, ids, context=None):
374 """Sets the 'anonymized' state to defined fields"""
376 # create a new history record:
377 anonymization_history_model = self.pool.get('ir.model.fields.anonymization.history')
380 'date': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
382 'direction': 'clear -> anonymized',
384 history_id = anonymization_history_model.create(cr, uid, vals)
386 # check that all the defined fields are in the 'clear' state
387 state = self.pool.get('ir.model.fields.anonymization')._get_global_state(cr, uid, context=context)
388 if state == 'anonymized':
389 self._raise_after_history_update(cr, uid, history_id, _('Error !'), _("The database is currently anonymized, you cannot anonymize it again."))
390 elif state == 'unstable':
391 msg = _("The database anonymization is currently in an unstable state. Some fields are anonymized," + \
392 " while some fields are not anonymized. You should try to solve this problem before trying to do anything.")
393 self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
395 # do the anonymization:
396 dirpath = os.environ.get('HOME') or os.getcwd()
397 rel_filepath = 'field_anonymization_%s_%s.pickle' % (cr.dbname, history_id)
398 abs_filepath = os.path.abspath(os.path.join(dirpath, rel_filepath))
400 ir_model_fields_anonymization_model = self.pool.get('ir.model.fields.anonymization')
401 field_ids = ir_model_fields_anonymization_model.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
402 fields = ir_model_fields_anonymization_model.browse(cr, uid, field_ids, context=context)
405 msg = "No fields are going to be anonymized."
406 self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
411 model_name = field.model_id.model
412 field_name = field.field_id.name
413 field_type = field.field_id.ttype
414 table_name = self.pool[model_name]._table
416 # get the current value
417 sql = "select id, %s from %s" % (field_name, table_name)
419 records = cr.dictfetchall()
420 for record in records:
421 data.append({"model_id": model_name, "field_id": field_name, "id": record['id'], "value": record[field_name]})
423 # anonymize the value:
424 anonymized_value = None
426 sid = str(record['id'])
427 if field_type == 'char':
428 anonymized_value = 'xxx'+sid
429 elif field_type == 'selection':
430 anonymized_value = 'xxx'+sid
431 elif field_type == 'text':
432 anonymized_value = 'xxx'+sid
433 elif field_type == 'boolean':
434 anonymized_value = random.choice([True, False])
435 elif field_type == 'date':
436 anonymized_value = '2011-11-11'
437 elif field_type == 'datetime':
438 anonymized_value = '2011-11-11 11:11:11'
439 elif field_type == 'float':
440 anonymized_value = 0.0
441 elif field_type == 'integer':
443 elif field_type in ['binary', 'many2many', 'many2one', 'one2many', 'reference']: # cannot anonymize these kind of fields
444 msg = _("Cannot anonymize fields of these types: binary, many2many, many2one, one2many, reference.")
445 self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
447 if anonymized_value is None:
448 self._raise_after_history_update(cr, uid, history_id, _('Error !'), _("Anonymized value is None. This cannot happens."))
450 sql = "update %(table)s set %(field)s = %%(anonymized_value)s where id = %%(id)s" % {
455 'anonymized_value': anonymized_value,
460 fn = open(abs_filepath, 'w')
461 pickle.dump(data, fn, pickle.HIGHEST_PROTOCOL)
463 # update the anonymization fields:
465 'state': 'anonymized',
467 ir_model_fields_anonymization_model.write(cr, uid, field_ids, values, context=context)
469 # add a result message in the wizard:
470 msgs = ["Anonymization successful.",
472 "Donot forget to save the resulting file to a safe place because you will not be able to revert the anonymization without this file.",
474 "This file is also stored in the %s directory. The absolute file path is: %s.",
476 msg = '\n'.join(msgs) % (dirpath, abs_filepath)
478 fn = open(abs_filepath, 'r')
480 self.write(cr, uid, ids, {
482 'file_export': base64.encodestring(fn.read()),
486 # update the history record:
487 anonymization_history_model.write(cr, uid, history_id, {
488 'field_ids': [[6, 0, field_ids]],
490 'filepath': abs_filepath,
495 view_id = self._id_get(cr, uid, 'ir.ui.view', 'view_ir_model_fields_anonymize_wizard_form', 'anonymization')
499 'view_id': [view_id],
502 'res_model': 'ir.model.fields.anonymize.wizard',
503 'type': 'ir.actions.act_window',
504 'context': {'step': 'just_anonymized'},
508 def reverse_anonymize_database(self, cr, uid, ids, context=None):
509 """Set the 'clear' state to defined fields"""
510 ir_model_fields_anonymization_model = self.pool.get('ir.model.fields.anonymization')
511 anonymization_history_model = self.pool.get('ir.model.fields.anonymization.history')
513 # create a new history record:
515 'date': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
517 'direction': 'anonymized -> clear',
519 history_id = anonymization_history_model.create(cr, uid, vals)
521 # check that all the defined fields are in the 'anonymized' state
522 state = ir_model_fields_anonymization_model._get_global_state(cr, uid, context=context)
524 raise osv.except_osv_('Error!', "The database is not currently anonymized, you cannot reverse the anonymization.")
525 elif state == 'unstable':
526 msg = _("The database anonymization is currently in an unstable state. Some fields are anonymized," + \
527 " while some fields are not anonymized. You should try to solve this problem before trying to do anything.")
528 raise osv.except_osv('Error!', msg)
530 wizards = self.browse(cr, uid, ids, context=context)
531 for wizard in wizards:
532 if not wizard.file_import:
533 msg = _("It is not possible to reverse the anonymization process without supplying the anonymization export file.")
534 self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
536 # reverse the anonymization:
537 # load the pickle file content into a data structure:
538 data = pickle.loads(base64.decodestring(wizard.file_import))
540 migration_fix_obj = self.pool.get('ir.model.fields.anonymization.migration.fix')
541 fix_ids = migration_fix_obj.search(cr, uid, [('target_version', '=', '7.0')])
542 fixes = migration_fix_obj.read(cr, uid, fix_ids, ['model_name', 'field_name', 'query', 'query_type', 'sequence'])
543 fixes = group(fixes, ('model_name', 'field_name'))
547 table_name = self.pool[line['model_id']]._table if line['model_id'] in self.pool else None
549 # check if custom sql exists:
550 key = (line['model_id'], line['field_id'])
551 custom_updates = fixes.get(key)
553 custom_updates.sort(key=itemgetter('sequence'))
554 queries = [(record['query'], record['query_type']) for record in custom_updates if record['query_type']]
556 queries = [("update %(table)s set %(field)s = %%(value)s where id = %%(id)s" % {
558 'field': line['field_id'],
561 for query in queries:
562 if query[1] == 'sql':
565 'value': line['value'],
568 elif query[1] == 'python':
570 code = raw_code % line
573 raise Exception("Unknown query type '%s'. Valid types are: sql, python." % (query['query_type'], ))
575 # update the anonymization fields:
576 ir_model_fields_anonymization_model = self.pool.get('ir.model.fields.anonymization')
577 field_ids = ir_model_fields_anonymization_model.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
581 ir_model_fields_anonymization_model.write(cr, uid, field_ids, values, context=context)
583 # add a result message in the wizard:
584 msg = '\n'.join(["Successfully reversed the anonymization.",
588 self.write(cr, uid, ids, {'msg': msg})
590 # update the history record:
591 anonymization_history_model.write(cr, uid, history_id, {
592 'field_ids': [[6, 0, field_ids]],
599 view_id = self._id_get(cr, uid, 'ir.ui.view', 'view_ir_model_fields_anonymize_wizard_form', 'anonymization')
603 'view_id': [view_id],
606 'res_model': 'ir.model.fields.anonymize.wizard',
607 'type': 'ir.actions.act_window',
608 'context': {'step': 'just_desanonymized'},
612 def _id_get(self, cr, uid, model, id_str, mod):
614 mod, id_str = id_str.split('.')
616 idn = self.pool.get('ir.model.data')._get_id(cr, uid, mod, id_str)
617 res = int(self.pool.get('ir.model.data').read(cr, uid, [idn], ['res_id'])[0]['res_id'])
623 class ir_model_fields_anonymization_migration_fix(osv.osv):
624 _name = 'ir.model.fields.anonymization.migration.fix'
628 'target_version': fields.char('Target Version'),
629 'model_name': fields.char('Model'),
630 'field_name': fields.char('Field'),
631 'query': fields.text('Query'),
632 'query_type': fields.selection(string='Query', selection=[('sql', 'sql'), ('python', 'python')]),
633 'sequence': fields.integer('Sequence'),
636 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: