diff --git a/addons/anonymization/__init__.py b/addons/anonymization/__init__.py
index 6ac286f9033..11529f98733 100644
--- a/addons/anonymization/__init__.py
+++ b/addons/anonymization/__init__.py
@@ -1,4 +1,5 @@
-# -*- encoding: utf-8 -*-
+# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import anonymization
+import models
+import wizard
diff --git a/addons/anonymization/__openerp__.py b/addons/anonymization/__openerp__.py
index 3f82cc44b45..1ddd445e9d9 100644
--- a/addons/anonymization/__openerp__.py
+++ b/addons/anonymization/__openerp__.py
@@ -1,4 +1,4 @@
-# -*- encoding: utf-8 -*-
+# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
@@ -19,11 +19,12 @@ team. Once you get back your migrated database, you restore it and reverse the
anonymization process to recover your previous data.
""",
'depends': ['base'],
- 'demo': ['anonymization_demo.xml'],
+ 'demo': ['data/anonymization_demo.xml'],
'data': [
- 'ir.model.fields.anonymization.csv',
+ 'data/ir.model.fields.anonymization.csv',
'security/ir.model.access.csv',
- 'anonymization_view.xml',
+ 'views/anonymization_views.xml',
+ 'wizard/anonymize_wizard_views.xml'
],
'installable': True,
'auto_install': False,
diff --git a/addons/anonymization/anonymization.py b/addons/anonymization/anonymization.py
deleted file mode 100644
index 5179850e7de..00000000000
--- a/addons/anonymization/anonymization.py
+++ /dev/null
@@ -1,625 +0,0 @@
-# -*- encoding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from lxml import etree
-import os
-import base64
-try:
- import cPickle as pickle
-except ImportError:
- import pickle
-import random
-import datetime
-from openerp.release import version_info
-from openerp.osv import fields, osv
-from openerp.tools.translate import _
-from openerp.tools.safe_eval import safe_eval as eval
-
-from itertools import groupby
-from operator import itemgetter
-from openerp.exceptions import UserError
-
-
-FIELD_STATES = [('clear', 'Clear'), ('anonymized', 'Anonymized'), ('not_existing', 'Not Existing'), ('new', 'New')]
-ANONYMIZATION_STATES = FIELD_STATES + [('unstable', 'Unstable')]
-WIZARD_ANONYMIZATION_STATES = [('clear', 'Clear'), ('anonymized', 'Anonymized'), ('unstable', 'Unstable')]
-ANONYMIZATION_HISTORY_STATE = [('started', 'Started'), ('done', 'Done'), ('in_exception', 'Exception occured')]
-ANONYMIZATION_DIRECTION = [('clear -> anonymized', 'clear -> anonymized'), ('anonymized -> clear', 'anonymized -> clear')]
-
-
-def group(lst, cols):
- if isinstance(cols, basestring):
- cols = [cols]
- return dict((k, [v for v in itr]) for k, itr in groupby(sorted(lst, key=itemgetter(*cols)), itemgetter(*cols)))
-
-
-class ir_model_fields_anonymization(osv.osv):
- _name = 'ir.model.fields.anonymization'
- _rec_name = 'field_id'
-
- _columns = {
- 'model_name': fields.char('Object Name', required=True),
- 'model_id': fields.many2one('ir.model', 'Object', ondelete='set null'),
- 'field_name': fields.char('Field Name', required=True),
- 'field_id': fields.many2one('ir.model.fields', 'Field', ondelete='set null'),
- 'state': fields.selection(selection=FIELD_STATES, String='Status', required=True, readonly=True),
- }
-
- _sql_constraints = [
- ('model_id_field_id_uniq', 'unique (model_name, field_name)', _("You cannot have two fields with the same name on the same object!")),
- ]
-
- def _get_global_state(self, cr, uid, context=None):
- ids = self.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
- fields = self.browse(cr, uid, ids, context=context)
- if not len(fields) or len(fields) == len([f for f in fields if f.state == 'clear']):
- state = 'clear' # all fields are clear
- elif len(fields) == len([f for f in fields if f.state == 'anonymized']):
- state = 'anonymized' # all fields are anonymized
- else:
- state = 'unstable' # fields are mixed: this should be fixed
-
- return state
-
- def _check_write(self, cr, uid, context=None):
- """check that the field is created from the menu and not from an database update
- otherwise the database update can crash:"""
- if context is None:
- context = {}
-
- if context.get('manual'):
- global_state = self._get_global_state(cr, uid, context=context)
- if global_state == 'anonymized':
- raise UserError(_("The database is currently anonymized, you cannot create, modify or delete fields."))
- elif global_state == 'unstable':
- msg = _("The database anonymization is currently in an unstable state. Some fields are anonymized,"
- " while some fields are not anonymized. You should try to solve this problem before trying to create, write or delete fields.")
- raise UserError(msg)
-
- return True
-
- def _get_model_and_field_ids(self, cr, uid, vals, context=None):
- model_and_field_ids = (False, False)
-
- if 'field_name' in vals and vals['field_name'] and 'model_name' in vals and vals['model_name']:
- ir_model_fields_obj = self.pool.get('ir.model.fields')
- ir_model_obj = self.pool.get('ir.model')
-
- model_ids = ir_model_obj.search(cr, uid, [('model', '=', vals['model_name'])], context=context)
- if model_ids:
- field_ids = ir_model_fields_obj.search(cr, uid, [('name', '=', vals['field_name']), ('model_id', '=', model_ids[0])], context=context)
- if field_ids:
- field_id = field_ids[0]
- model_and_field_ids = (model_ids[0], field_id)
-
- return model_and_field_ids
-
- def create(self, cr, uid, vals, context=None):
- # check field state: all should be clear before we can add a new field to anonymize:
- self._check_write(cr, uid, context=context)
-
- global_state = self._get_global_state(cr, uid, context=context)
-
- if 'field_name' in vals and vals['field_name'] and 'model_name' in vals and vals['model_name']:
- vals['model_id'], vals['field_id'] = self._get_model_and_field_ids(cr, uid, vals, context=context)
-
- # check not existing fields:
- if not vals.get('field_id'):
- vals['state'] = 'not_existing'
- else:
- vals['state'] = global_state
-
- res = super(ir_model_fields_anonymization, self).create(cr, uid, vals, context=context)
-
- return res
-
- def write(self, cr, uid, ids, vals, context=None):
- # check field state: all should be clear before we can modify a field:
- if not (len(vals.keys()) == 1 and vals.get('state') == 'clear'):
- self._check_write(cr, uid, context=context)
-
- if 'field_name' in vals and vals['field_name'] and 'model_name' in vals and vals['model_name']:
- vals['model_id'], vals['field_id'] = self._get_model_and_field_ids(cr, uid, vals, context=context)
-
- # check not existing fields:
- if 'field_id' in vals:
- if not vals.get('field_id'):
- vals['state'] = 'not_existing'
- else:
- global_state = self._get_global_state(cr, uid, context)
- if global_state != 'unstable':
- vals['state'] = global_state
-
- res = super(ir_model_fields_anonymization, self).write(cr, uid, ids, vals, context=context)
-
- return res
-
- def unlink(self, cr, uid, ids, context=None):
- # check field state: all should be clear before we can unlink a field:
- self._check_write(cr, uid, context=context)
-
- res = super(ir_model_fields_anonymization, self).unlink(cr, uid, ids, context=context)
- return res
-
- def onchange_model_id(self, cr, uid, ids, model_id, context=None):
- res = {'value': {
- 'field_name': False,
- 'field_id': False,
- 'model_name': False,
- }}
-
- if model_id:
- ir_model_obj = self.pool.get('ir.model')
- model_ids = ir_model_obj.search(cr, uid, [('id', '=', model_id)])
- model_id = model_ids and model_ids[0] or None
- model_name = model_id and ir_model_obj.browse(cr, uid, model_id).model or False
- res['value']['model_name'] = model_name
-
- return res
-
- def onchange_model_name(self, cr, uid, ids, model_name, context=None):
- res = {'value': {
- 'field_name': False,
- 'field_id': False,
- 'model_id': False,
- }}
-
- if model_name:
- ir_model_obj = self.pool.get('ir.model')
- model_ids = ir_model_obj.search(cr, uid, [('model', '=', model_name)])
- model_id = model_ids and model_ids[0] or False
- res['value']['model_id'] = model_id
-
- return res
-
- def onchange_field_name(self, cr, uid, ids, field_name, model_name):
- res = {'value': {
- 'field_id': False,
- }}
-
- if field_name and model_name:
- ir_model_fields_obj = self.pool.get('ir.model.fields')
- field_ids = ir_model_fields_obj.search(cr, uid, [('name', '=', field_name), ('model', '=', model_name)])
- field_id = field_ids and field_ids[0] or False
- res['value']['field_id'] = field_id
-
- return res
-
- def onchange_field_id(self, cr, uid, ids, field_id, model_name):
- res = {'value': {
- 'field_name': False,
- }}
-
- if field_id:
- ir_model_fields_obj = self.pool.get('ir.model.fields')
- field = ir_model_fields_obj.browse(cr, uid, field_id)
- res['value']['field_name'] = field.name
-
- return res
-
- _defaults = {
- 'state': lambda *a: 'clear',
- }
-
-
-class ir_model_fields_anonymization_history(osv.osv):
- _name = 'ir.model.fields.anonymization.history'
- _order = "date desc"
-
- _columns = {
- 'date': fields.datetime('Date', required=True, readonly=True),
- 'field_ids': fields.many2many('ir.model.fields.anonymization', 'anonymized_field_to_history_rel', 'field_id', 'history_id', 'Fields', readonly=True),
- 'state': fields.selection(selection=ANONYMIZATION_HISTORY_STATE, string='Status', required=True, readonly=True),
- 'direction': fields.selection(selection=ANONYMIZATION_DIRECTION, string='Direction', size=20, required=True, readonly=True),
- 'msg': fields.text('Message', readonly=True),
- 'filepath': fields.char(string='File path', readonly=True),
- }
-
-
-class ir_model_fields_anonymize_wizard(osv.osv_memory):
- _name = 'ir.model.fields.anonymize.wizard'
-
- def _get_state(self, cr, uid, ids, name, arg, context=None):
- res = {}
-
- state = self._get_state_value(cr, uid, context=None)
- for id in ids:
- res[id] = state
-
- return res
-
- def _get_summary(self, cr, uid, ids, name, arg, context=None):
- res = {}
- summary = self._get_summary_value(cr, uid, context)
- for id in ids:
- res[id] = summary
-
- return res
-
- _columns = {
- 'name': fields.char(string='File Name'),
- 'summary': fields.function(_get_summary, type='text', string='Summary'),
- 'file_export': fields.binary(string='Export'),
- 'file_import': fields.binary(string='Import', help="This is the file created by the anonymization process. It should have the '.pickle' extention."),
- 'state': fields.function(_get_state, string='Status', type='selection', selection=WIZARD_ANONYMIZATION_STATES, readonly=False),
- 'msg': fields.text(string='Message'),
- }
-
- def _get_state_value(self, cr, uid, context=None):
- state = self.pool.get('ir.model.fields.anonymization')._get_global_state(cr, uid, context=context)
- return state
-
- def _get_summary_value(self, cr, uid, context=None):
- summary = u''
- anon_field_obj = self.pool.get('ir.model.fields.anonymization')
- ir_model_fields_obj = self.pool.get('ir.model.fields')
-
- anon_field_ids = anon_field_obj.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
- anon_fields = anon_field_obj.browse(cr, uid, anon_field_ids, context=context)
-
- field_ids = [anon_field.field_id.id for anon_field in anon_fields if anon_field.field_id]
- fields = ir_model_fields_obj.browse(cr, uid, field_ids, context=context)
-
- fields_by_id = dict([(f.id, f) for f in fields])
-
- for anon_field in anon_fields:
- field = fields_by_id.get(anon_field.field_id.id)
-
- values = {
- 'model_name': field.model_id.name,
- 'model_code': field.model_id.model,
- 'field_code': field.name,
- 'field_name': field.field_description,
- 'state': anon_field.state,
- }
- summary += u" * %(model_name)s (%(model_code)s) -> %(field_name)s (%(field_code)s): state: (%(state)s)\n" % values
-
- return summary
-
- def default_get(self, cr, uid, fields_list, context=None):
- res = {}
- res['name'] = '.pickle'
- res['summary'] = self._get_summary_value(cr, uid, context)
- res['state'] = self._get_state_value(cr, uid, context)
- res['msg'] = _("""Before executing the anonymization process, you should make a backup of your database.""")
-
- return res
-
- def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
- state = self.pool.get('ir.model.fields.anonymization')._get_global_state(cr, uid, context=context)
-
- if context is None:
- context = {}
-
- step = context.get('step', 'new_window')
-
- res = super(ir_model_fields_anonymize_wizard, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
-
- eview = etree.fromstring(res['arch'])
- placeholder = eview.xpath("group[@name='placeholder1']")
- if len(placeholder):
- placeholder = placeholder[0]
- if step == 'new_window' and state == 'clear':
- # clicked in the menu and the fields are not anonymized: warn the admin that backuping the db is very important
- placeholder.addnext(etree.Element('field', {'name': 'msg', 'colspan': '4', 'nolabel': '1'}))
- placeholder.addnext(etree.Element('newline'))
- placeholder.addnext(etree.Element('label', {'string': 'Warning'}))
- eview.remove(placeholder)
- elif step == 'new_window' and state == 'anonymized':
- # clicked in the menu and the fields are already anonymized
- placeholder.addnext(etree.Element('newline'))
- placeholder.addnext(etree.Element('field', {'name': 'file_import', 'required': "1"}))
- placeholder.addnext(etree.Element('label', {'string': 'Anonymization file'}))
- eview.remove(placeholder)
- elif step == 'just_anonymized':
- # we just ran the anonymization process, we need the file export field
- placeholder.addnext(etree.Element('newline'))
- placeholder.addnext(etree.Element('field', {'name': 'file_export'}))
- # we need to remove the button:
- buttons = eview.xpath("button")
- for button in buttons:
- eview.remove(button)
- # and add a message:
- placeholder.addnext(etree.Element('field', {'name': 'msg', 'colspan': '4', 'nolabel': '1'}))
- placeholder.addnext(etree.Element('newline'))
- placeholder.addnext(etree.Element('label', {'string': 'Result'}))
- # remove the placeholer:
- eview.remove(placeholder)
- elif step == 'just_desanonymized':
- # we just reversed the anonymization process, we don't need any field
- # we need to remove the button
- buttons = eview.xpath("button")
- for button in buttons:
- eview.remove(button)
- # and add a message
- # and add a message:
- placeholder.addnext(etree.Element('field', {'name': 'msg', 'colspan': '4', 'nolabel': '1'}))
- placeholder.addnext(etree.Element('newline'))
- placeholder.addnext(etree.Element('label', {'string': 'Result'}))
- # remove the placeholer:
- eview.remove(placeholder)
- else:
- msg = _("The database anonymization is currently in an unstable state. Some fields are anonymized,"
- " while some fields are not anonymized. You should try to solve this problem before trying to do anything else.")
- raise UserError(msg)
-
- res['arch'] = etree.tostring(eview)
-
- return res
-
- def _raise_after_history_update(self, cr, uid, history_id, error_type, error_msg):
- self.pool.get('ir.model.fields.anonymization.history').write(cr, uid, history_id, {
- 'state': 'in_exception',
- 'msg': error_msg,
- })
- raise UserError('%s: %s' % (error_type, error_msg))
-
- def anonymize_database(self, cr, uid, ids, context=None):
- """Sets the 'anonymized' state to defined fields"""
-
- # create a new history record:
- anonymization_history_model = self.pool.get('ir.model.fields.anonymization.history')
-
- vals = {
- 'date': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- 'state': 'started',
- 'direction': 'clear -> anonymized',
- }
- history_id = anonymization_history_model.create(cr, uid, vals)
-
- # check that all the defined fields are in the 'clear' state
- state = self.pool.get('ir.model.fields.anonymization')._get_global_state(cr, uid, context=context)
- if state == 'anonymized':
- self._raise_after_history_update(cr, uid, history_id, _('Error !'), _("The database is currently anonymized, you cannot anonymize it again."))
- elif state == 'unstable':
- msg = _("The database anonymization is currently in an unstable state. Some fields are anonymized,"
- " while some fields are not anonymized. You should try to solve this problem before trying to do anything.")
- self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
-
- # do the anonymization:
- dirpath = os.environ.get('HOME') or os.getcwd()
- rel_filepath = 'field_anonymization_%s_%s.pickle' % (cr.dbname, history_id)
- abs_filepath = os.path.abspath(os.path.join(dirpath, rel_filepath))
-
- ir_model_fields_anonymization_model = self.pool.get('ir.model.fields.anonymization')
- field_ids = ir_model_fields_anonymization_model.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
- fields = ir_model_fields_anonymization_model.browse(cr, uid, field_ids, context=context)
-
- if not fields:
- msg = "No fields are going to be anonymized."
- self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
-
- data = []
-
- for field in fields:
- model_name = field.model_id.model
- field_name = field.field_id.name
- field_type = field.field_id.ttype
- table_name = self.pool[model_name]._table
-
- # get the current value
- sql = "select id, %s from %s" % (field_name, table_name)
- cr.execute(sql)
- records = cr.dictfetchall()
- for record in records:
- data.append({"model_id": model_name, "field_id": field_name, "id": record['id'], "value": record[field_name]})
-
- # anonymize the value:
- anonymized_value = None
-
- sid = str(record['id'])
- if field_type == 'char':
- anonymized_value = 'xxx'+sid
- elif field_type == 'selection':
- anonymized_value = 'xxx'+sid
- elif field_type == 'text':
- anonymized_value = 'xxx'+sid
- elif field_type == 'html':
- anonymized_value = 'xxx'+sid
- elif field_type == 'boolean':
- anonymized_value = random.choice([True, False])
- elif field_type == 'date':
- anonymized_value = '2011-11-11'
- elif field_type == 'datetime':
- anonymized_value = '2011-11-11 11:11:11'
- elif field_type == 'float':
- anonymized_value = 0.0
- elif field_type == 'integer':
- anonymized_value = 0
- elif field_type in ['binary', 'many2many', 'many2one', 'one2many', 'reference']: # cannot anonymize these kind of fields
- msg = _("Cannot anonymize fields of these types: binary, many2many, many2one, one2many, reference.")
- self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
-
- if anonymized_value is None:
- self._raise_after_history_update(cr, uid, history_id, _('Error !'), _("Anonymized value can not be empty."))
-
- sql = "update %(table)s set %(field)s = %%(anonymized_value)s where id = %%(id)s" % {
- 'table': table_name,
- 'field': field_name,
- }
- cr.execute(sql, {
- 'anonymized_value': anonymized_value,
- 'id': record['id']
- })
-
- # save pickle:
- fn = open(abs_filepath, 'w')
- pickle.dump(data, fn, pickle.HIGHEST_PROTOCOL)
-
- # update the anonymization fields:
- values = {
- 'state': 'anonymized',
- }
- ir_model_fields_anonymization_model.write(cr, uid, field_ids, values, context=context)
-
- # add a result message in the wizard:
- msgs = ["Anonymization successful.",
- "",
- "Donot forget to save the resulting file to a safe place because you will not be able to revert the anonymization without this file.",
- "",
- "This file is also stored in the %s directory. The absolute file path is: %s.",
- ]
- msg = '\n'.join(msgs) % (dirpath, abs_filepath)
-
- fn = open(abs_filepath, 'r')
-
- self.write(cr, uid, ids, {
- 'msg': msg,
- 'file_export': base64.encodestring(fn.read()),
- })
- fn.close()
-
- # update the history record:
- anonymization_history_model.write(cr, uid, history_id, {
- 'field_ids': [[6, 0, field_ids]],
- 'msg': msg,
- 'filepath': abs_filepath,
- 'state': 'done',
- })
-
- # handle the view:
- view_id = self.pool['ir.model.data'].xmlid_to_res_id(
- cr, uid, 'anonymization.view_ir_model_fields_anonymize_wizard_form'
- )
-
- return {
- 'res_id': ids[0],
- 'view_id': [view_id],
- 'view_type': 'form',
- "view_mode": 'form',
- 'res_model': 'ir.model.fields.anonymize.wizard',
- 'type': 'ir.actions.act_window',
- 'context': {'step': 'just_anonymized'},
- 'target':'new',
- }
-
- def reverse_anonymize_database(self, cr, uid, ids, context=None):
- """Set the 'clear' state to defined fields"""
- ir_model_fields_anonymization_model = self.pool.get('ir.model.fields.anonymization')
- anonymization_history_model = self.pool.get('ir.model.fields.anonymization.history')
-
- # create a new history record:
- vals = {
- 'date': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- 'state': 'started',
- 'direction': 'anonymized -> clear',
- }
- history_id = anonymization_history_model.create(cr, uid, vals)
-
- # check that all the defined fields are in the 'anonymized' state
- state = ir_model_fields_anonymization_model._get_global_state(cr, uid, context=context)
- if state == 'clear':
- raise UserError(_("The database is not currently anonymized, you cannot reverse the anonymization."))
- elif state == 'unstable':
- msg = _("The database anonymization is currently in an unstable state. Some fields are anonymized,"
- " while some fields are not anonymized. You should try to solve this problem before trying to do anything.")
- raise UserError(msg)
-
- wizards = self.browse(cr, uid, ids, context=context)
- for wizard in wizards:
- if not wizard.file_import:
- msg = _("It is not possible to reverse the anonymization process without supplying the anonymization export file.")
- self._raise_after_history_update(cr, uid, history_id, 'Error !', msg)
-
- # reverse the anonymization:
- # load the pickle file content into a data structure:
- data = pickle.loads(base64.decodestring(wizard.file_import))
-
- migration_fix_obj = self.pool.get('ir.model.fields.anonymization.migration.fix')
- fix_ids = migration_fix_obj.search(cr, uid, [('target_version', '=', '.'.join(map(str, version_info[:2])))])
- fixes = migration_fix_obj.read(cr, uid, fix_ids, ['model_name', 'field_name', 'query', 'query_type', 'sequence'])
- fixes = group(fixes, ('model_name', 'field_name'))
-
- for line in data:
- queries = []
- table_name = self.pool[line['model_id']]._table if line['model_id'] in self.pool else None
-
- # check if custom sql exists:
- key = (line['model_id'], line['field_id'])
- custom_updates = fixes.get(key)
- if custom_updates:
- custom_updates.sort(key=itemgetter('sequence'))
- queries = [(record['query'], record['query_type']) for record in custom_updates if record['query_type']]
- elif table_name:
- queries = [("update %(table)s set %(field)s = %%(value)s where id = %%(id)s" % {
- 'table': table_name,
- 'field': line['field_id'],
- }, 'sql')]
-
- for query in queries:
- if query[1] == 'sql':
- sql = query[0]
- cr.execute(sql, {
- 'value': line['value'],
- 'id': line['id']
- })
- elif query[1] == 'python':
- raw_code = query[0]
- code = raw_code % line
- eval(code)
- else:
- raise Exception("Unknown query type '%s'. Valid types are: sql, python." % (query['query_type'], ))
-
- # update the anonymization fields:
- ir_model_fields_anonymization_model = self.pool.get('ir.model.fields.anonymization')
- field_ids = ir_model_fields_anonymization_model.search(cr, uid, [('state', '<>', 'not_existing')], context=context)
- values = {
- 'state': 'clear',
- }
- ir_model_fields_anonymization_model.write(cr, uid, field_ids, values, context=context)
-
- # add a result message in the wizard:
- msg = '\n'.join(["Successfully reversed the anonymization.",
- "",
- ])
-
- self.write(cr, uid, ids, {'msg': msg})
-
- # update the history record:
- anonymization_history_model.write(cr, uid, history_id, {
- 'field_ids': [[6, 0, field_ids]],
- 'msg': msg,
- 'filepath': False,
- 'state': 'done',
- })
-
- # handle the view:
- view_id = self.pool['ir.model.data'].xmlid_to_res_id(
- cr, uid, 'anonymization.view_ir_model_fields_anonymize_wizard_form'
- )
-
-
- return {
- 'res_id': ids[0],
- 'view_id': [view_id],
- 'view_type': 'form',
- "view_mode": 'form',
- 'res_model': 'ir.model.fields.anonymize.wizard',
- 'type': 'ir.actions.act_window',
- 'context': {'step': 'just_desanonymized'},
- 'target':'new',
- }
-
- def _id_get(self, cr, uid, model, id_str, mod):
- if '.' in id_str:
- mod, id_str = id_str.split('.')
- try:
- idn = self.pool.get('ir.model.data')._get_id(cr, uid, mod, id_str)
- res = int(self.pool.get('ir.model.data').read(cr, uid, [idn], ['res_id'])[0]['res_id'])
- except:
- res = None
- return res
-
-
-class ir_model_fields_anonymization_migration_fix(osv.osv):
- _name = 'ir.model.fields.anonymization.migration.fix'
- _order = "sequence"
-
- _columns = {
- 'target_version': fields.char('Target Version'),
- 'model_name': fields.char('Model'),
- 'field_name': fields.char('Field'),
- 'query': fields.text('Query'),
- 'query_type': fields.selection(string='Query', selection=[('sql', 'sql'), ('python', 'python')]),
- 'sequence': fields.integer('Sequence'),
- }
diff --git a/addons/anonymization/anonymization_demo.xml b/addons/anonymization/data/anonymization_demo.xml
similarity index 93%
rename from addons/anonymization/anonymization_demo.xml
rename to addons/anonymization/data/anonymization_demo.xml
index 1183cc975ea..91a107bae26 100644
--- a/addons/anonymization/anonymization_demo.xml
+++ b/addons/anonymization/data/anonymization_demo.xml
@@ -1,5 +1,5 @@
-
-
+
+
@@ -18,5 +18,5 @@
-
+
diff --git a/addons/anonymization/ir.model.fields.anonymization.csv b/addons/anonymization/data/ir.model.fields.anonymization.csv
similarity index 100%
rename from addons/anonymization/ir.model.fields.anonymization.csv
rename to addons/anonymization/data/ir.model.fields.anonymization.csv
diff --git a/addons/anonymization/models/__init__.py b/addons/anonymization/models/__init__.py
new file mode 100644
index 00000000000..5d2a1a794f0
--- /dev/null
+++ b/addons/anonymization/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import anonymization
diff --git a/addons/anonymization/models/anonymization.py b/addons/anonymization/models/anonymization.py
new file mode 100644
index 00000000000..06ffcf92216
--- /dev/null
+++ b/addons/anonymization/models/anonymization.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from itertools import groupby
+from operator import itemgetter
+
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+
+FIELD_STATES = [('clear', 'Clear'), ('anonymized', 'Anonymized'), ('not_existing', 'Not Existing'), ('new', 'New')]
+ANONYMIZATION_HISTORY_STATE = [('started', 'Started'), ('done', 'Done'), ('in_exception', 'Exception occured')]
+ANONYMIZATION_DIRECTION = [('clear -> anonymized', 'clear -> anonymized'), ('anonymized -> clear', 'anonymized -> clear')]
+
+
+def group(lst, cols):
+ if isinstance(cols, basestring):
+ cols = [cols]
+ return dict((k, [v for v in itr]) for k, itr in groupby(sorted(lst, key=itemgetter(*cols)), itemgetter(*cols)))
+
+
+class IrModelFieldsAnonymization(models.Model):
+ _name = 'ir.model.fields.anonymization'
+ _rec_name = 'field_id'
+
+ model_name = fields.Char('Object Name', required=True)
+ model_id = fields.Many2one('ir.model', string='Object', ondelete='set null')
+ field_name = fields.Char(required=True)
+ field_id = fields.Many2one('ir.model.fields', string='Field', ondelete='set null')
+ state = fields.Selection(selection=FIELD_STATES, string='Status', required=True, readonly=True, default='clear')
+
+ _sql_constraints = [
+ ('model_id_field_id_uniq', 'unique (model_name, field_name)', _("You cannot have two fields with the same name on the same object!")),
+ ]
+
+ @api.model
+ def _get_global_state(self):
+ field_ids = self.search([('state', '!=', 'not_existing')])
+ if not field_ids or len(field_ids) == len(field_ids.filtered(lambda field: field.state == "clear")):
+ state = 'clear' # all fields are clear
+ elif len(field_ids) == len(field_ids.filtered(lambda field: field.state == "anonymized")):
+ state = 'anonymized' # all fields are anonymized
+ else:
+ state = 'unstable' # fields are mixed: this should be fixed
+ return state
+
+ @api.model
+ def _check_write(self):
+ """check that the field is created from the menu and not from an database update
+ otherwise the database update can crash:"""
+ if self.env.context.get('manual'):
+ global_state = self._get_global_state()
+ if global_state == 'anonymized':
+ raise UserError(_("The database is currently anonymized, you cannot create, modify or delete fields."))
+ elif global_state == 'unstable':
+ raise UserError(_("The database anonymization is currently in an unstable state. Some fields are anonymized,"
+ " while some fields are not anonymized. You should try to solve this problem before trying to create, write or delete fields."))
+ return True
+
+ @api.model
+ def _get_model_and_field_ids(self, vals):
+ if vals.get('field_name') and vals.get('model_name'):
+ model_id = self.env['ir.model'].search([('model', '=', vals['model_name'])], limit=1).id
+ if model_id:
+ field_id = self.env['ir.model.fields'].search([('name', '=', vals['field_name']), ('model_id', '=', model_id)], limit=1).id
+ if field_id:
+ return (model_id, field_id)
+ return (False, False)
+
+ @api.model
+ def create(self, vals):
+ # check field state: all should be clear before we can add a new field to anonymize:
+ self._check_write()
+ if vals.get('field_name') and vals.get('model_name'):
+ vals['model_id'], vals['field_id'] = self._get_model_and_field_ids(vals)
+ # check not existing fields:
+ vals['state'] = self._get_global_state() if vals.get('field_id') else 'not_existing'
+ return super(IrModelFieldsAnonymization, self).create(vals)
+
+ @api.multi
+ def write(self, vals):
+ # check field state: all should be clear before we can modify a field:
+ if not len(vals.keys()) == 1 and vals.get('state') == 'clear':
+ self._check_write()
+ if vals.get('field_name') and vals.get('model_name'):
+ vals['model_id'], vals['field_id'] = self._get_model_and_field_ids(vals)
+ # check not existing fields:
+ if 'field_id' in vals:
+ if not vals['field_id']:
+ vals['state'] = 'not_existing'
+ else:
+ global_state = self._get_global_state()
+ if global_state != 'unstable':
+ vals['state'] = global_state
+ return super(IrModelFieldsAnonymization, self).write(vals)
+
+ @api.multi
+ def unlink(self):
+ # check field state: all should be clear before we can unlink a field:
+ self._check_write()
+ return super(IrModelFieldsAnonymization, self).unlink()
+
+ @api.onchange('model_id')
+ def _onchange_model_id(self):
+ self.field_name = False
+ self.field_id = False
+ self.model_name = self.model_id.model
+
+ @api.onchange('model_name')
+ def _onchange_model_name(self):
+ self.field_name = False
+ self.field_id = False
+ if self.model_name:
+ self.model_id = self.env['ir.model'].search([('model', '=', self.model_name)], limit=1)
+ else:
+ self.model_id = False
+
+ @api.onchange('field_name')
+ def _onchange_field_name(self):
+ if self.field_name and self.model_name:
+ self.field_id = self.env['ir.model.fields'].search([
+ ('name', '=', self.field_name), ('model', '=', self.model_name)
+ ], limit=1)
+ else:
+ self.field_id = False
+
+ @api.onchange('field_id')
+ def _onchange_field_id(self):
+ self.field_name = self.field_id.name
+
+
+class IrModelFieldsAnonymizationHistory(models.Model):
+ _name = 'ir.model.fields.anonymization.history'
+ _order = "date desc"
+
+ date = fields.Datetime(required=True, readonly=True)
+ field_ids = fields.Many2many(
+ 'ir.model.fields.anonymization', 'anonymized_field_to_history_rel',
+ 'field_id', 'history_id', string='Fields', readonly=True
+ )
+ state = fields.Selection(selection=ANONYMIZATION_HISTORY_STATE, string='Status', required=True, readonly=True)
+ direction = fields.Selection(selection=ANONYMIZATION_DIRECTION, required=True, readonly=True)
+ msg = fields.Text('Message', readonly=True)
+ filepath = fields.Char('File path', readonly=True)
+
+
+class IrModelFieldsAnonymizationMigrationFix(models.Model):
+ _name = 'ir.model.fields.anonymization.migration.fix'
+ _order = "sequence"
+
+ target_version = fields.Char('Target Version')
+ model_name = fields.Char('Model')
+ field_name = fields.Char('Field')
+ query = fields.Text()
+ query_type = fields.Selection(selection=[('sql', 'sql'), ('python', 'python')], string='Query')
+ sequence = fields.Integer()
diff --git a/addons/anonymization/anonymization_view.xml b/addons/anonymization/views/anonymization_views.xml
similarity index 58%
rename from addons/anonymization/anonymization_view.xml
rename to addons/anonymization/views/anonymization_views.xml
index 6b3ca271976..df81688365a 100644
--- a/addons/anonymization/anonymization_view.xml
+++ b/addons/anonymization/views/anonymization_views.xml
@@ -1,22 +1,20 @@
-
-
-
+
+
-
+
ir.model.fields.anonymization.form
ir.model.fields.anonymization
-
+
ir.model.fields.anonymization.tree
ir.model.fields.anonymization
@@ -38,7 +36,7 @@
-
+
Anonymized Fields
ir.model.fields.anonymization
form
@@ -46,55 +44,16 @@
tree,form
-
-
-
-
- ir.model.fields.anonymize.wizard.form
- ir.model.fields.anonymize.wizard
-
-
-
-
-
-
-
-
- Anonymize Database
- ir.model.fields.anonymize.wizard
- new
- form
- form
-
-
-
+
Anonymization History
ir.model.fields.anonymization.history
form
tree,form
-
+
ir.model.fields.anonymization.history.form
ir.model.fields.anonymization.history
@@ -122,7 +81,7 @@
-
+
ir.model.fields.anonymization.history.tree
ir.model.fields.anonymization.history
@@ -154,11 +113,4 @@
parent="menu_administration_anonymization"
sequence="20" />
-
-
-
-
+
diff --git a/addons/anonymization/wizard/__init__.py b/addons/anonymization/wizard/__init__.py
new file mode 100644
index 00000000000..d22126ebe0d
--- /dev/null
+++ b/addons/anonymization/wizard/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import anonymize_wizard
diff --git a/addons/anonymization/wizard/anonymize_wizard.py b/addons/anonymization/wizard/anonymize_wizard.py
new file mode 100644
index 00000000000..8879ce5dc65
--- /dev/null
+++ b/addons/anonymization/wizard/anonymize_wizard.py
@@ -0,0 +1,338 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import base64
+import os
+import random
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+from lxml import etree
+from operator import itemgetter
+
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+from odoo.release import version_info
+from odoo.tools.safe_eval import safe_eval as eval
+from odoo.addons.anonymization.models.anonymization import group
+
+WIZARD_ANONYMIZATION_STATES = [('clear', 'Clear'), ('anonymized', 'Anonymized'), ('unstable', 'Unstable')]
+
+
+class IrModelFieldsAnonymizeWizard(models.TransientModel):
+ _name = 'ir.model.fields.anonymize.wizard'
+
+ name = fields.Char('File Name')
+ summary = fields.Text(compute='_compute_summary')
+ file_export = fields.Binary('Export')
+ file_import = fields.Binary('Import',
+ help="This is the file created by the anonymization process. It should have the '.pickle' extention.")
+ state = fields.Selection(compute='_compute_state', string='Status', selection=WIZARD_ANONYMIZATION_STATES)
+ msg = fields.Text('Message')
+
+ @api.multi
+ def _compute_summary(self):
+ for anonymize_wizard in self:
+ anonymize_wizard.summary = anonymize_wizard._get_summary_value()
+
+ @api.multi
+ def _compute_state(self):
+ for anonymize_wizard in self:
+ anonymize_wizard.state = anonymize_wizard._get_state_value()
+
+ @api.model
+ def _get_state_value(self):
+ return self.env['ir.model.fields.anonymization']._get_global_state()
+
+ @api.model
+ def _get_summary_value(self):
+ summary = u''
+ for anon_field in self.env['ir.model.fields.anonymization'].search([('state', '!=', 'not_existing')]):
+ field = anon_field.field_id
+ values = {
+ 'model_name': field.model_id.name,
+ 'model_code': field.model_id.model,
+ 'field_code': field.name,
+ 'field_name': field.field_description,
+ 'state': anon_field.state,
+ }
+ summary += u" * %(model_name)s (%(model_code)s) -> %(field_name)s (%(field_code)s): state: (%(state)s)\n" % values
+ return summary
+
+ @api.model
+ def default_get(self, fields_list):
+ res = {}
+ res['name'] = '.pickle'
+ res['summary'] = self._get_summary_value()
+ res['state'] = self._get_state_value()
+ res['msg'] = _("Before executing the anonymization process, you should make a backup of your database.")
+ return res
+
+ @api.model
+ def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
+ state = self.env['ir.model.fields.anonymization']._get_global_state()
+ step = self.env.context.get('step', 'new_window')
+ res = super(IrModelFieldsAnonymizeWizard, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
+ eview = etree.fromstring(res['arch'])
+ placeholder = eview.xpath("group[@name='placeholder1']")
+ if len(placeholder):
+ placeholder = placeholder[0]
+ if step == 'new_window' and state == 'clear':
+ # clicked in the menu and the fields are not anonymized: warn the admin that backuping the db is very important
+ placeholder.addnext(etree.Element('field', {'name': 'msg', 'colspan': '4', 'nolabel': '1'}))
+ placeholder.addnext(etree.Element('newline'))
+ placeholder.addnext(etree.Element('label', {'string': 'Warning'}))
+ eview.remove(placeholder)
+ elif step == 'new_window' and state == 'anonymized':
+ # clicked in the menu and the fields are already anonymized
+ placeholder.addnext(etree.Element('newline'))
+ placeholder.addnext(etree.Element('field', {'name': 'file_import', 'required': "1"}))
+ placeholder.addnext(etree.Element('label', {'string': 'Anonymization file'}))
+ eview.remove(placeholder)
+ elif step == 'just_anonymized':
+ # we just ran the anonymization process, we need the file export field
+ placeholder.addnext(etree.Element('newline'))
+ placeholder.addnext(etree.Element('field', {'name': 'file_export'}))
+ # we need to remove the button:
+ buttons = eview.xpath("button")
+ for button in buttons:
+ eview.remove(button)
+ # and add a message:
+ placeholder.addnext(etree.Element('field', {'name': 'msg', 'colspan': '4', 'nolabel': '1'}))
+ placeholder.addnext(etree.Element('newline'))
+ placeholder.addnext(etree.Element('label', {'string': 'Result'}))
+ # remove the placeholer:
+ eview.remove(placeholder)
+ elif step == 'just_desanonymized':
+ # we just reversed the anonymization process, we don't need any field
+ # we need to remove the button
+ buttons = eview.xpath("button")
+ for button in buttons:
+ eview.remove(button)
+ # and add a message
+ placeholder.addnext(etree.Element('field', {'name': 'msg', 'colspan': '4', 'nolabel': '1'}))
+ placeholder.addnext(etree.Element('newline'))
+ placeholder.addnext(etree.Element('label', {'string': 'Result'}))
+ # remove the placeholer:
+ eview.remove(placeholder)
+ else:
+ raise UserError(_("The database anonymization is currently in an unstable state. Some fields are anonymized,"
+ " while some fields are not anonymized. You should try to solve this problem before trying to do anything else."))
+ res['arch'] = etree.tostring(eview)
+ return res
+
+ def _raise_after_history_update(self, history, error_type, error_msg):
+ history.write({
+ 'state': 'in_exception',
+ 'msg': error_msg,
+ })
+ raise UserError('%s: %s' % (error_type, error_msg))
+
+ @api.multi
+ def anonymize_database(self):
+ """Sets the 'anonymized' state to defined fields"""
+ self.ensure_one()
+
+ # create a new history record:
+ history = self.env['ir.model.fields.anonymization.history'].create({
+ 'date': fields.Datetime.now(),
+ 'state': 'started',
+ 'direction': 'clear -> anonymized'
+ })
+
+ # check that all the defined fields are in the 'clear' state
+ state = self.env['ir.model.fields.anonymization']._get_global_state()
+ if state == 'anonymized':
+ self._raise_after_history_update(history, _('Error !'), _("The database is currently anonymized, you cannot anonymize it again."))
+ elif state == 'unstable':
+ self._raise_after_history_update(history, _('Error !'), _("The database anonymization is currently in an unstable state. Some fields are anonymized,"
+ " while some fields are not anonymized. You should try to solve this problem before trying to do anything."))
+
+ # do the anonymization:
+ dirpath = os.environ.get('HOME') or os.getcwd()
+ rel_filepath = 'field_anonymization_%s_%s.pickle' % (self.env.cr.dbname, history.id)
+ abs_filepath = os.path.abspath(os.path.join(dirpath, rel_filepath))
+
+ ano_fields = self.env['ir.model.fields.anonymization'].search([('state', '!=', 'not_existing')])
+ if not ano_fields:
+ self._raise_after_history_update(history, _('Error !'), _("No fields are going to be anonymized."))
+
+ data = []
+
+ for field in ano_fields:
+ model_name = field.model_id.model
+ field_name = field.field_id.name
+ field_type = field.field_id.ttype
+ table_name = self.env[model_name]._table
+
+ # get the current value
+ self.env.cr.execute("select id, %s from %s" % (field_name, table_name))
+ for record in self.env.cr.dictfetchall():
+ data.append({"model_id": model_name, "field_id": field_name, "id": record['id'], "value": record[field_name]})
+
+ # anonymize the value:
+ anonymized_value = None
+
+ sid = str(record['id'])
+ if field_type == 'char':
+ anonymized_value = 'xxx' + sid
+ elif field_type == 'selection':
+ anonymized_value = 'xxx' + sid
+ elif field_type == 'text':
+ anonymized_value = 'xxx' + sid
+ elif field_type == 'html':
+ anonymized_value = 'xxx' + sid
+ elif field_type == 'boolean':
+ anonymized_value = random.choice([True, False])
+ elif field_type == 'date':
+ anonymized_value = '2011-11-11'
+ elif field_type == 'datetime':
+ anonymized_value = '2011-11-11 11:11:11'
+ elif field_type == 'float':
+ anonymized_value = 0.0
+ elif field_type == 'integer':
+ anonymized_value = 0
+ elif field_type in ['binary', 'many2many', 'many2one', 'one2many', 'reference']: # cannot anonymize these kind of fields
+ self._raise_after_history_update(history, _('Error !'), _("Cannot anonymize fields of these types: binary, many2many, many2one, one2many, reference."))
+
+ if anonymized_value is None:
+ self._raise_after_history_update(history, _('Error !'), _("Anonymized value can not be empty."))
+
+ sql = "update %(table)s set %(field)s = %%(anonymized_value)s where id = %%(id)s" % {
+ 'table': table_name,
+ 'field': field_name,
+ }
+ self.env.cr.execute(sql, {
+ 'anonymized_value': anonymized_value,
+ 'id': record['id']
+ })
+
+ # save pickle:
+ fn = open(abs_filepath, 'w')
+ pickle.dump(data, fn, pickle.HIGHEST_PROTOCOL)
+
+ # update the anonymization fields:
+ ano_fields.write({'state': 'anonymized'})
+
+ # add a result message in the wizard:
+ msgs = ["Anonymization successful.",
+ "",
+ "Donot forget to save the resulting file to a safe place because you will not be able to revert the anonymization without this file.",
+ "",
+ "This file is also stored in the %s directory. The absolute file path is: %s."
+ ]
+ msg = '\n'.join(msgs) % (dirpath, abs_filepath)
+
+ fn = open(abs_filepath, 'r')
+
+ self.write({
+ 'msg': msg,
+ 'file_export': base64.encodestring(fn.read()),
+ })
+ fn.close()
+
+ # update the history record:
+ history.write({
+ 'field_ids': [[6, 0, ano_fields.ids]],
+ 'msg': msg,
+ 'filepath': abs_filepath,
+ 'state': 'done',
+ })
+
+ return {
+ 'res_id': self.id,
+ 'view_id': self.env.ref('anonymization.view_ir_model_fields_anonymize_wizard_form').ids,
+ 'view_type': 'form',
+ "view_mode": 'form',
+ 'res_model': 'ir.model.fields.anonymize.wizard',
+ 'type': 'ir.actions.act_window',
+ 'context': {'step': 'just_anonymized'},
+ 'target': 'new'
+ }
+
+ @api.multi
+ def reverse_anonymize_database(self):
+ """Set the 'clear' state to defined fields"""
+ self.ensure_one()
+ IrModelFieldsAnonymization = self.env['ir.model.fields.anonymization']
+
+ # create a new history record:
+ history = self.env['ir.model.fields.anonymization.history'].create({
+ 'date': fields.Datetime.now(),
+ 'state': 'started',
+ 'direction': 'anonymized -> clear'
+ })
+
+ # check that all the defined fields are in the 'anonymized' state
+ state = IrModelFieldsAnonymization._get_global_state()
+ if state == 'clear':
+ raise UserError(_("The database is not currently anonymized, you cannot reverse the anonymization."))
+ elif state == 'unstable':
+ raise UserError(_("The database anonymization is currently in an unstable state. Some fields are anonymized,"
+ " while some fields are not anonymized. You should try to solve this problem before trying to do anything."))
+
+ if not self.file_import:
+ raise UserError('%s: %s' % (_('Error !'), _("It is not possible to reverse the anonymization process without supplying the anonymization export file.")))
+
+ # reverse the anonymization:
+ # load the pickle file content into a data structure:
+ data = pickle.loads(base64.decodestring(self.file_import))
+
+ fixes = self.env['ir.model.fields.anonymization.migration.fix'].search_read([
+ ('target_version', '=', '.'.join(map(str, version_info[:2])))
+ ], ['model_name', 'field_name', 'query', 'query_type', 'sequence'])
+ fixes = group(fixes, ('model_name', 'field_name'))
+
+ for line in data:
+ queries = []
+ table_name = self.env[line['model_id']]._table if line['model_id'] in self.env else None
+
+ # check if custom sql exists:
+ key = (line['model_id'], line['field_id'])
+ custom_updates = fixes.get(key)
+ if custom_updates:
+ custom_updates.sort(key=itemgetter('sequence'))
+ queries = [(record['query'], record['query_type']) for record in custom_updates if record['query_type']]
+ elif table_name:
+ queries = [("update %(table)s set %(field)s = %%(value)s where id = %%(id)s" % {
+ 'table': table_name,
+ 'field': line['field_id'],
+ }, 'sql')]
+
+ for query in queries:
+ if query[1] == 'sql':
+ self.env.cr.execute(query[0], {
+ 'value': line['value'],
+ 'id': line['id']
+ })
+ elif query[1] == 'python':
+ eval(query[0] % line)
+ else:
+ raise Exception("Unknown query type '%s'. Valid types are: sql, python." % (query['query_type'], ))
+
+ # update the anonymization fields:
+ ano_fields = IrModelFieldsAnonymization.search([('state', '!=', 'not_existing')])
+ ano_fields.write({'state': 'clear'})
+
+ # add a result message in the wizard:
+ self.msg = '\n'.join(["Successfully reversed the anonymization.", ""])
+
+ # update the history record:
+ history.write({
+ 'field_ids': [[6, 0, ano_fields.ids]],
+ 'msg': self.msg,
+ 'filepath': False,
+ 'state': 'done'
+ })
+
+ return {
+ 'res_id': self.id,
+ 'view_id': self.env.ref('anonymization.view_ir_model_fields_anonymize_wizard_form').ids,
+ 'view_type': 'form',
+ "view_mode": 'form',
+ 'res_model': 'ir.model.fields.anonymize.wizard',
+ 'type': 'ir.actions.act_window',
+ 'context': {'step': 'just_desanonymized'},
+ 'target': 'new'
+ }
diff --git a/addons/anonymization/wizard/anonymize_wizard_views.xml b/addons/anonymization/wizard/anonymize_wizard_views.xml
new file mode 100644
index 00000000000..abe577e855b
--- /dev/null
+++ b/addons/anonymization/wizard/anonymize_wizard_views.xml
@@ -0,0 +1,44 @@
+
+
+
+
+ ir.model.fields.anonymize.wizard.form
+ ir.model.fields.anonymize.wizard
+
+
+
+
+
+
+ Anonymize Database
+ ir.model.fields.anonymize.wizard
+ new
+ form
+ form
+
+
+
+
+
diff --git a/addons/google_drive/views/google_drive_views.xml b/addons/google_drive/views/google_drive_views.xml
index 5dde683f301..bc0ff8f23e5 100644
--- a/addons/google_drive/views/google_drive_views.xml
+++ b/addons/google_drive/views/google_drive_views.xml
@@ -22,7 +22,7 @@
-
+