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
35 from osv import fields, osv
36 from tools.translate import _
39 FIELD_STATES = [('clear', 'Clear'), ('anonymized', 'Anonymized'), ('not_existing', 'Not Existing')]
40 ANONYMIZATION_STATES = FIELD_STATES + [('unstable', 'Unstable')]
41 ANONYMIZATION_HISTORY_STATE = [('started', 'Started'), ('done', 'Done'), ('in_exception', 'Exception occured')]
42 ANONYMIZATION_DIRECTION = [('clear -> anonymized', 'clear -> anonymized'), ('anonymized -> clear', 'anonymized -> clear')]
45 class ir_model_fields_anonymization(osv.osv):
46 _name = 'ir.model.fields.anonymization'
47 _rec_name = 'field_id'
50 'model_name': fields.char('Object Name', size=128, required=True),
51 'model_id': fields.many2one('ir.model', 'Object', ondelete='set null'),
52 'field_name': fields.char('Field Name', size=128, required=True),
53 'field_id': fields.many2one('ir.model.fields', 'Field', ondelete='set null'),
54 'state': fields.selection(selection=FIELD_STATES, String='State', required=True, readonly=True),
58 ('model_id_field_id_uniq', 'unique (model_name, field_name)', _("You cannot have two records having the same model and the same field")),
61 def _get_global_state(self, cr, uid, context=None):
62 ids = self.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
63 fields = self.browse(cr, uid, ids, context=context)
64 if not len(fields) or len(fields) == len([f for f in fields if f.state == 'clear']):
65 state = 'clear' # all fields are clear
66 elif len(fields) == len([f for f in fields if f.state == 'anonymized']):
67 state = 'anonymized' # all fields are anonymized
69 state = 'unstable' # fields are mixed: this should be fixed
72 def _check_write(self, cr, uid, context=None):
73 # check that the field is created from the menu and not from an database update
74 # otherwise the database update can crash:
75 if context.get('manual'):
76 global_state = self._get_global_state(cr, uid, context=context)
77 if global_state == 'anonymized':
78 raise osv.except_osv('Error !', "The database is currently anonymized, you cannot create, modify or delete fields.")
79 elif global_state == 'unstable':
80 msg = "The database anonymization is currently in an unstable state. Some fields are anonymized," + \
81 " while some fields are not anonymized. You should try to solve this problem before trying to create, write or delete fields."
82 raise osv.except_osv('Error !', msg)
86 def _get_model_and_field_ids(self, cr, uid, vals, context=None):
87 model_and_field_ids = (False, False)
89 if 'field_name' in vals and vals['field_name'] and 'model_name' in vals and vals['model_name']:
90 ir_model_fields_obj = self.pool.get('ir.model.fields')
91 ir_model_obj = self.pool.get('ir.model')
93 model_ids = ir_model_obj.search(cr, uid, [('model', '=', vals['model_name'])], context=context)
95 field_ids = ir_model_fields_obj.search(cr, uid, [('name', '=', vals['field_name']), ('model_id', '=', model_ids[0])], context=context)
97 field_id = field_ids[0]
98 model_and_field_ids = (model_ids[0], field_id)
100 return model_and_field_ids
102 def create(self, cr, uid, vals, context=None):
103 # check field state: all should be clear before we can add a new field to anonymize:
104 self._check_write(cr, uid, context=context)
106 if 'field_name' in vals and vals['field_name'] and 'model_name' in vals and vals['model_name']:
107 vals['model_id'], vals['field_id'] = self._get_model_and_field_ids(cr, uid, vals, context=context)
109 # check not existing fields:
110 if not vals.get('field_id'):
111 vals['state'] = 'not_existing'
113 res = super(ir_model_fields_anonymization, self).create(cr, uid, vals, context=context)
117 def write(self, cr, uid, ids, vals, context=None):
118 # check field state: all should be clear before we can modify a field:
119 if not (len(vals.keys()) == 1 and vals.get('state') == 'clear'):
120 self._check_write(cr, uid, context=context)
122 if 'field_name' in vals and vals['field_name'] and 'model_name' in vals and vals['model_name']:
123 vals['model_id'], vals['field_id'] = self._get_model_and_field_ids(cr, uid, vals, context=context)
125 # check not existing fields:
126 if 'field_id' in vals:
127 if not vals.get('field_id'):
128 vals['state'] = 'not_existing'
130 global_state = self._get_global_state(cr, uid, context)
131 if global_state != 'unstable':
132 vals['state'] = global_state
134 res = super(ir_model_fields_anonymization, self).write(cr, uid, ids, vals, context=context)
138 def unlink(self, cr, uid, ids, context=None):
139 # check field state: all should be clear before we can unlink a field:
140 self._check_write(cr, uid, context=context)
142 res = super(ir_model_fields_anonymization, self).unlink(cr, uid, ids, context=context)
145 def onchange_model_id(self, cr, uid, ids, model_id, context=None):
153 ir_model_obj = self.pool.get('ir.model')
154 model_ids = ir_model_obj.search(cr, uid, [('id', '=', model_id)])
155 model_id = model_ids and model_ids[0] or None
156 model_name = model_id and ir_model_obj.browse(cr, uid, model_id).model or False
157 res['value']['model_name'] = model_name
161 def onchange_model_name(self, cr, uid, ids, model_name, context=None):
169 ir_model_obj = self.pool.get('ir.model')
170 model_ids = ir_model_obj.search(cr, uid, [('model', '=', model_name)])
171 model_id = model_ids and model_ids[0] or False
172 res['value']['model_id'] = model_id
176 def onchange_field_name(self, cr, uid, ids, field_name, model_name):
181 if field_name and model_name:
182 ir_model_fields_obj = self.pool.get('ir.model.fields')
183 field_ids = ir_model_fields_obj.search(cr, uid, [('name', '=', field_name), ('model', '=', model_name)])
184 field_id = field_ids and field_ids[0] or False
185 res['value']['field_id'] = field_id
189 def onchange_field_id(self, cr, uid, ids, field_id, model_name):
195 ir_model_fields_obj = self.pool.get('ir.model.fields')
196 field = ir_model_fields_obj.browse(cr, uid, field_id)
197 res['value']['field_name'] = field.name
202 'state': lambda *a: 'clear',
205 ir_model_fields_anonymization()
208 class ir_model_fields_anonymization_history(osv.osv):
209 _name = 'ir.model.fields.anonymization.history'
213 'date': fields.datetime('Date', required=True, readonly=True),
214 'field_ids': fields.many2many('ir.model.fields.anonymization', 'anonymized_field_to_history_rel', 'field_id', 'history_id', 'Fields', readonly=True),
215 'state': fields.selection(selection=ANONYMIZATION_HISTORY_STATE, string='State', required=True, readonly=True),
216 'direction': fields.selection(selection=ANONYMIZATION_DIRECTION, string='Direction', required=True, readonly=True),
217 'msg': fields.text('Message', readonly=True),
218 'filepath': fields.char(string='File path', size=256, readonly=True),
221 ir_model_fields_anonymization_history()
224 class ir_model_fields_anonymize_wizard(osv.osv_memory):
225 _name = 'ir.model.fields.anonymize.wizard'
227 def _get_state(self, cr, uid, ids, name, arg, context=None):
230 state = self._get_state_value(cr, uid, context=None)
236 def _get_summary(self, cr, uid, ids, name, arg, context=None):
238 summary = self._get_summary_value(cr, uid, context)
245 'name': fields.char(size='64', string='File Name'),
246 'summary': fields.function(_get_summary, method=True, type='text', string='Summary'),
247 'file_export': fields.binary(string='Export'),
248 'file_import': fields.binary(string='Import'),
249 'state': fields.function(_get_state, method=True, string='State', type='selection', selection=ANONYMIZATION_STATES, readonly=False),
250 'msg': fields.text(string='Message'),
253 def _get_state_value(self, cr, uid, context=None):
254 state = self.pool.get('ir.model.fields.anonymization')._get_global_state(cr, uid, context=context)
257 def _get_summary_value(self, cr, uid, context=None):
259 anon_field_obj = self.pool.get('ir.model.fields.anonymization')
260 ir_model_obj = self.pool.get('ir.model')
261 ir_model_fields_obj = self.pool.get('ir.model.fields')
263 anon_field_ids = anon_field_obj.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
264 anon_fields = anon_field_obj.browse(cr, uid, anon_field_ids, context=context)
266 field_ids = [anon_field.field_id.id for anon_field in anon_fields if anon_field.field_id]
267 fields = ir_model_fields_obj.browse(cr, uid, field_ids, context=context)
269 fields_by_id = dict([(f.id, f) for f in fields])
271 for anon_field in anon_fields:
272 field = fields_by_id.get(anon_field.field_id.id)
275 'model_name': field.model_id.name,
276 'model_code': field.model_id.model,
277 'field_code': field.name,
278 'field_name': field.field_description,
279 'state': anon_field.state,
281 summary += u" * %(model_name)s (%(model_code)s) -> %(field_name)s (%(field_code)s): state: (%(state)s)\n" % values
285 def default_get(self, cr, uid, fields_list, context=None):
287 res['name'] = '.pickle'
288 res['summary'] = self._get_summary_value(cr, uid, context)
289 res['state'] = self._get_state_value(cr, uid, context)
290 res['msg'] = """Before executing the anonymization process, you should make a backup of your database."""
294 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, *args, **kwargs):
295 state = self.pool.get('ir.model.fields.anonymization')._get_global_state(cr, uid, context=context)
296 step = context.get('step', 'new_window')
298 res = super(ir_model_fields_anonymize_wizard, self).fields_view_get(cr, uid, view_id, view_type, context, *args, **kwargs)
300 eview = etree.fromstring(res['arch'])
301 placeholder = eview.xpath("group[@name='placeholder1']")
302 placeholder = len(placeholder) and placeholder[0] or None
305 if step == 'new_window' and state == 'clear':
306 # clicked in the menu and the fields are not anonymized: warn the admin that backuping the db is very important
307 placeholder.addnext(etree.Element('field', {'name': 'msg', 'colspan': '4', 'nolabel': '1'}))
308 placeholder.addnext(etree.Element('newline'))
309 placeholder.addnext(etree.Element('label', {'string': 'Warning'}))
310 eview.remove(placeholder)
311 elif step == 'new_window' and state == 'anonymized':
312 # clicked in the menu and the fields are already anonymized
313 placeholder.addnext(etree.Element('newline'))
314 placeholder.addnext(etree.Element('field', {'name': 'file_import', 'required': "1"}))
315 eview.remove(placeholder)
316 elif step == 'just_anonymized':
317 # we just ran the anonymization process, we need the file export field
318 placeholder.addnext(etree.Element('newline'))
319 placeholder.addnext(etree.Element('field', {'name': 'file_export'}))
320 # we need to remove the button:
321 buttons = eview.xpath("button")
322 for button in buttons:
325 placeholder.addnext(etree.Element('field', {'name': 'msg', 'colspan': '4', 'nolabel': '1'}))
326 placeholder.addnext(etree.Element('newline'))
327 placeholder.addnext(etree.Element('label', {'string': 'Result'}))
328 # remove the placeholer:
329 eview.remove(placeholder)
330 elif step == 'just_desanonymized':
331 # we just reversed the anonymization process, we don't need any field
332 # we need to remove the button
333 buttons = eview.xpath("button")
334 for button in buttons:
338 placeholder.addnext(etree.Element('field', {'name': 'msg', 'colspan': '4', 'nolabel': '1'}))
339 placeholder.addnext(etree.Element('newline'))
340 placeholder.addnext(etree.Element('label', {'string': 'Result'}))
341 # remove the placeholer:
342 eview.remove(placeholder)
344 from olilib.openerp import Terp, ppt, pst
345 import pydb; pydb.debugger(['set listsize 40'])
351 res['arch'] = etree.tostring(eview)
355 def _raise_after_history_update(self, cr, uid, history_id, error_type, error_msg):
356 self.pool.get('ir.model.fields.anonymization.history').write(cr, uid, history_id, {
357 'state': 'in_exception',
360 raise osv.except_osv(error_type, error_msg)
362 def anonymize_database(self,cr, uid, ids, context=None):
363 """Sets the 'anonymized' state to defined fields"""
365 # create a new history record:
366 anonymization_history_model = self.pool.get('ir.model.fields.anonymization.history')
369 'date': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
371 'direction': 'clear -> anonymized',
373 history_id = anonymization_history_model.create(cr, uid, vals)
375 # check that all the defined fields are in the 'clear' state
376 state = self.pool.get('ir.model.fields.anonymization')._get_global_state(cr, uid, context=context)
377 if state == 'anonymized':
378 self._raise_after_history_update(cr, uid, history_id, 'Error !', "The database is currently anonymized, you cannot anonymize it again.")
379 elif state == 'unstable':
380 msg = "The database anonymization is currently in an unstable state. Some fields are anonymized," + \
381 " while some fields are not anonymized. You should try to solve this problem before trying to do anything."
382 self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
384 # do the anonymization:
385 dirpath = os.environ.get('HOME') or os.getcwd()
386 rel_filepath = 'field_anonymization_%s_%s.pickle' % (cr.dbname, history_id)
387 abs_filepath = os.path.abspath(os.path.join(dirpath, rel_filepath))
389 ir_model_fields_anonymization_model = self.pool.get('ir.model.fields.anonymization')
390 field_ids = ir_model_fields_anonymization_model.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
391 fields = ir_model_fields_anonymization_model.browse(cr, uid, field_ids, context=context)
394 msg = "No fields are going to be anonymized."
395 self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
400 model_name = field.model_id.model
401 field_name = field.field_id.name
402 field_type = field.field_id.ttype
403 table_name = self.pool.get(model_name)._table
405 # get the current value
406 sql = "select id, %s from %s" % (field_name, table_name)
408 records = cr.dictfetchall()
409 for record in records:
410 data.append({"model_id": model_name, "field_id": field_name, "id": record['id'], "value": record[field_name]})
412 # anonymize the value:
413 anonymized_value = None
415 sid = str(record['id'])
416 if field_type == 'char':
417 anonymized_value = 'xxx'+sid
418 elif field_type == 'selection':
419 anonymized_value = 'xxx'+sid
420 elif field_type == 'text':
421 anonymized_value = 'xxx'+sid
422 elif field_type == 'boolean':
423 anonymized_value = random.choice([True, False])
424 elif field_type == 'date':
425 anonymized_value = '2011-11-11'
426 elif field_type == 'datetime':
427 anonymized_value = '2011-11-11 11:11:11'
428 elif field_type == 'float':
429 if record[field_name] > 0:
430 anonymized_value = 1.0
431 elif record[field_name] < 0:
432 anonymized_value = -1.0
434 anonymized_value = 0.0
435 elif field_type == 'integer':
437 elif field_type in ['binary', 'many2many', 'many2one', 'one2many', 'reference']: # cannot anonymize these kind of fields
438 msg = "Cannot anonymize fields of these types: binary, many2many, many2one, one2many, reference"
439 self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
441 if anonymized_value is None:
442 self._raise_after_history_update(cr, uid, history_id, 'Error !', "Anonymized value is None. This cannot happens.")
444 sql = "update %(table)s set %(field)s = %%(anonymized_value)s where id = %%(id)s" % {
449 'anonymized_value': anonymized_value,
454 fn = open(abs_filepath, 'w')
455 pickle.dump(data, fn, pickle.HIGHEST_PROTOCOL)
457 # update the anonymization fields:
459 'state': 'anonymized',
461 res = ir_model_fields_anonymization_model.write(cr, uid, field_ids, values, context=context)
463 # add a result message in the wizard:
464 msgs = ["Anonymization successful.",
466 "Don't forget to save the resulting file to a safe place because you will not be able to revert the anonymization without this file.",
468 "This file is also stored in the %s directory. The absolute file path is: %s",
470 msg = '\n'.join(msgs) % (dirpath, abs_filepath)
472 fn = open(abs_filepath, 'r')
474 self.write(cr, uid, ids, {
476 'file_export': base64.encodestring(fn.read()),
480 # update the history record:
481 anonymization_history_model.write(cr, uid, history_id, {
482 'field_ids': [[6, 0, field_ids]],
484 'filepath': abs_filepath,
489 view_id = self._id_get(cr, uid, 'ir.ui.view', 'view_ir_model_fields_anonymize_wizard_form', 'anonymization')
493 'view_id': [view_id],
496 'res_model': 'ir.model.fields.anonymize.wizard',
497 'type': 'ir.actions.act_window',
498 'context': {'step': 'just_anonymized'},
502 def reverse_anonymize_database(self,cr, uid, ids, context=None):
503 """Set the 'clear' state to defined fields"""
505 ir_model_fields_anonymization_model = self.pool.get('ir.model.fields.anonymization')
506 anonymization_history_model = self.pool.get('ir.model.fields.anonymization.history')
508 # create a new history record:
510 'date': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
512 'direction': 'anonymized -> clear',
514 history_id = anonymization_history_model.create(cr, uid, vals)
516 # check that all the defined fields are in the 'anonymized' state
517 state = ir_model_fields_anonymization_model._get_global_state(cr, uid, context=context)
519 raise osv.except_osv('Error !', "The database is not currently anonymized, you cannot reverse the anonymization.")
520 elif state == 'unstable':
521 msg = "The database anonymization is currently in an unstable state. Some fields are anonymized," + \
522 " while some fields are not anonymized. You should try to solve this problem before trying to do anything."
523 raise osv.except_osv('Error !', msg)
525 wizards = self.browse(cr, uid, ids, context=context)
526 for wizard in wizards:
527 if not wizard.file_import:
528 msg = "The anonymization export file was not supplied. It is not possible to reverse the anonymization process without this file."
529 self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
531 # reverse the anonymization:
532 # load the pickle file content into a data structure:
533 data = pickle.loads(base64.decodestring(wizard.file_import))
536 table_name = self.pool.get(line['model_id'])._table
537 sql = "update %(table)s set %(field)s = %%(value)s where id = %%(id)s" % {
539 'field': line['field_id'],
542 'value': line['value'],
546 # update the anonymization fields:
547 ir_model_fields_anonymization_model = self.pool.get('ir.model.fields.anonymization')
548 field_ids = ir_model_fields_anonymization_model.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
552 res = ir_model_fields_anonymization_model.write(cr, uid, field_ids, values, context=context)
554 # add a result message in the wizard:
555 msg = '\n'.join(["Successfully reversed the anonymization.",
559 self.write(cr, uid, ids, {'msg': msg})
561 # update the history record:
562 anonymization_history_model.write(cr, uid, history_id, {
563 'field_ids': [[6, 0, field_ids]],
570 view_id = self._id_get(cr, uid, 'ir.ui.view', 'view_ir_model_fields_anonymize_wizard_form', 'anonymization')
574 'view_id': [view_id],
577 'res_model': 'ir.model.fields.anonymize.wizard',
578 'type': 'ir.actions.act_window',
579 'context': {'step': 'just_desanonymized'},
583 def _id_get(self, cr, uid, model, id_str, mod):
585 mod, id_str = id_str.split('.')
587 idn = self.pool.get('ir.model.data')._get_id(cr, uid, mod, id_str)
588 res = int(self.pool.get('ir.model.data').read(cr, uid, [idn], ['res_id'])[0]['res_id'])
593 ir_model_fields_anonymize_wizard()