Browse Source

[ADD] migrate database_cleanup

pull/469/head
Holger Brunn 8 years ago
parent
commit
ece76e72a3
No known key found for this signature in database GPG Key ID: 1C9760FECA3AE18
  1. 73
      database_cleanup/README.rst
  2. 5
      database_cleanup/__init__.py
  3. 41
      database_cleanup/__openerp__.py
  4. 155
      database_cleanup/model/purge_columns.py
  5. 106
      database_cleanup/model/purge_data.py
  6. 81
      database_cleanup/model/purge_menus.py
  7. 155
      database_cleanup/model/purge_models.py
  8. 129
      database_cleanup/model/purge_modules.py
  9. 87
      database_cleanup/model/purge_wizard.py
  10. 0
      database_cleanup/models/__init__.py
  11. 123
      database_cleanup/models/purge_columns.py
  12. 71
      database_cleanup/models/purge_data.py
  13. 48
      database_cleanup/models/purge_menus.py
  14. 120
      database_cleanup/models/purge_models.py
  15. 79
      database_cleanup/models/purge_modules.py
  16. 89
      database_cleanup/models/purge_tables.py
  17. 77
      database_cleanup/models/purge_wizard.py
  18. 4
      database_cleanup/tests/__init__.py
  19. 50
      database_cleanup/tests/test_database_cleanup.py
  20. 0
      database_cleanup/views/menu.xml
  21. 35
      database_cleanup/views/purge_columns.xml
  22. 35
      database_cleanup/views/purge_data.xml
  23. 34
      database_cleanup/views/purge_menus.xml
  24. 35
      database_cleanup/views/purge_models.xml
  25. 35
      database_cleanup/views/purge_modules.xml
  26. 31
      database_cleanup/views/purge_tables.xml
  27. 41
      database_cleanup/views/purge_wizard.xml

73
database_cleanup/README.rst

@ -1,15 +1,78 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
================
atabase cleanup
================
Clean your OpenERP database from remnants of modules, models, columns and Clean your OpenERP database from remnants of modules, models, columns and
tables left by uninstalled modules (prior to 7.0) or a homebrew database tables left by uninstalled modules (prior to 7.0) or a homebrew database
upgrade to a new major version of OpenERP. upgrade to a new major version of OpenERP.
Caution! This module is potentially harmful and can *easily* destroy the
integrity of your data. Do not use if you are not entirely comfortable
with the technical details of the OpenERP data model of *all* the modules
that have ever been installed on your database, and do not purge any module,
model, column or table if you do not know exactly what you are doing.
Usage
=====
After installation of this module, go to the Settings menu -> Technical -> After installation of this module, go to the Settings menu -> Technical ->
Database cleanup. Go through the modules, models, columns and tables Database cleanup. Go through the modules, models, columns and tables
entries under this menu (in that order) and find out if there is orphaned data entries under this menu (in that order) and find out if there is orphaned data
in your database. You can either delete entries by line, or sweep all entries in your database. You can either delete entries by line, or sweep all entries
in one big step (if you are *really* confident). in one big step (if you are *really* confident).
Caution! This module is potentially harmful and can *easily* destroy the
integrity of your data. Do not use if you are not entirely comfortable
with the technical details of the OpenERP data model of *all* the modules
that have ever been installed on your database, and do not purge any module,
model, column or table if you do not know exactly what you are doing.
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/149/9.0
For further information, please visit:
* https://www.odoo.com/forum/help-1
Known issues / Roadmap
======================
* ...
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/database_cleanup/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback
`here <https://github.com/OCA/database_cleanup/issues/new?body=module:%20database_cleanup%0Aversion:%209.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Stefan Rijnhart <stefan@opener.amsterdam>
* Holger Brunn <hbrunn@therp.nl>
Do not contact contributors directly about help with questions or problems concerning this addon, but use the `community mailing list <mailto:community@mail.odoo.com>`_ or the `appropriate specialized mailinglist <https://odoo-community.org/groups>`_ for help, and the bug tracker linked in `Bug Tracker`_ above for technical issues.
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit https://odoo-community.org.

5
database_cleanup/__init__.py

@ -1 +1,4 @@
from . import model
# -*- coding: utf-8 -*-
# © 2014-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import models

41
database_cleanup/__openerp__.py

@ -1,39 +1,22 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
# © 2014-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{ {
'name': 'Database cleanup', 'name': 'Database cleanup',
'version': '8.0.0.1.0',
'version': '9.0.1.0.0',
'author': "Therp BV,Odoo Community Association (OCA)", 'author': "Therp BV,Odoo Community Association (OCA)",
'depends': ['base'], 'depends': ['base'],
'license': 'AGPL-3', 'license': 'AGPL-3',
'category': 'Tools', 'category': 'Tools',
'data': [ 'data': [
'view/purge_menus.xml',
'view/purge_modules.xml',
'view/purge_models.xml',
'view/purge_columns.xml',
'view/purge_tables.xml',
'view/purge_data.xml',
'view/menu.xml',
"views/purge_wizard.xml",
'views/purge_menus.xml',
'views/purge_modules.xml',
'views/purge_models.xml',
'views/purge_columns.xml',
'views/purge_tables.xml',
'views/purge_data.xml',
'views/menu.xml',
], ],
'installable': False,
'installable': True,
} }

155
database_cleanup/model/purge_columns.py

@ -1,155 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
from openerp.tools.translate import _
class CleanupPurgeLineColumn(orm.TransientModel):
_inherit = 'cleanup.purge.line'
_name = 'cleanup.purge.line.column'
_columns = {
'model_id': fields.many2one(
'ir.model', 'Model',
required=True, ondelete='CASCADE'),
'wizard_id': fields.many2one(
'cleanup.purge.wizard.column', 'Purge Wizard', readonly=True),
}
def purge(self, cr, uid, ids, context=None):
"""
Unlink columns upon manual confirmation.
"""
for line in self.browse(cr, uid, ids, context=context):
if line.purged:
continue
model_pool = self.pool[line.model_id.model]
# Check whether the column actually still exists.
# Inheritance such as stock.picking.in from stock.picking
# can lead to double attempts at removal
cr.execute(
'SELECT count(attname) FROM pg_attribute '
'WHERE attrelid = '
'( SELECT oid FROM pg_class WHERE relname = %s ) '
'AND attname = %s',
(model_pool._table, line.name))
if not cr.fetchone()[0]:
continue
self.logger.info(
'Dropping column %s from table %s',
line.name, model_pool._table)
cr.execute(
"""
ALTER TABLE "%s" DROP COLUMN "%s"
""" % (model_pool._table, line.name))
line.write({'purged': True})
cr.commit()
return True
class CleanupPurgeWizardColumn(orm.TransientModel):
_inherit = 'cleanup.purge.wizard'
_name = 'cleanup.purge.wizard.column'
# List of known columns in use without corresponding fields
# Format: {table: [fields]}
blacklist = {
'wkf_instance': ['uid'], # lp:1277899
}
def default_get(self, cr, uid, fields, context=None):
res = super(CleanupPurgeWizardColumn, self).default_get(
cr, uid, fields, context=context)
if 'name' in fields:
res['name'] = _('Purge columns')
return res
def get_orphaned_columns(self, cr, uid, model_pools, context=None):
"""
From openobject-server/openerp/osv/orm.py
Iterate on the database columns to identify columns
of fields which have been removed
"""
columns = list(set([
column for model_pool in model_pools
for column in model_pool._columns
if not (isinstance(model_pool._columns[column],
fields.function) and
not model_pool._columns[column].store)
]))
columns += orm.MAGIC_COLUMNS
columns += self.blacklist.get(model_pools[0]._table, [])
cr.execute("SELECT a.attname"
" FROM pg_class c, pg_attribute a"
" WHERE c.relname=%s"
" AND c.oid=a.attrelid"
" AND a.attisdropped=%s"
" AND pg_catalog.format_type(a.atttypid, a.atttypmod)"
" NOT IN ('cid', 'tid', 'oid', 'xid')"
" AND a.attname NOT IN %s",
(model_pools[0]._table, False, tuple(columns))),
return [column[0] for column in cr.fetchall()]
def find(self, cr, uid, context=None):
"""
Search for columns that are not in the corresponding model.
Group models by table to prevent false positives for columns
that are only in some of the models sharing the same table.
Example of this is 'sale_id' not being a field of stock.picking.in
"""
res = []
model_pool = self.pool['ir.model']
model_ids = model_pool.search(cr, uid, [], context=context)
# mapping of tables to tuples (model id, [pool1, pool2, ...])
table2model = {}
for model in model_pool.browse(cr, uid, model_ids, context=context):
model_pool = self.pool.get(model.model)
if not model_pool or not model_pool._auto:
continue
table2model.setdefault(
model_pool._table, (model.id, []))[1].append(model_pool)
for table, model_spec in table2model.iteritems():
for column in self.get_orphaned_columns(
cr, uid, model_spec[1], context=context):
res.append((0, 0, {
'name': column,
'model_id': model_spec[0]}))
if not res:
raise orm.except_orm(
_('Nothing to do'),
_('No orphaned columns found'))
return res
_columns = {
'purge_line_ids': fields.one2many(
'cleanup.purge.line.column',
'wizard_id', 'Columns to purge'),
}

106
database_cleanup/model/purge_data.py

@ -1,106 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
from openerp.tools.translate import _
class CleanupPurgeLineData(orm.TransientModel):
_inherit = 'cleanup.purge.line'
_name = 'cleanup.purge.line.data'
_columns = {
'data_id': fields.many2one(
'ir.model.data', 'Data entry',
ondelete='SET NULL'),
'wizard_id': fields.many2one(
'cleanup.purge.wizard.data', 'Purge Wizard', readonly=True),
}
def purge(self, cr, uid, ids, context=None):
"""
Unlink data entries upon manual confirmation.
"""
data_ids = []
for line in self.browse(cr, uid, ids, context=context):
if line.purged or not line.data_id:
continue
data_ids.append(line.data_id.id)
self.logger.info('Purging data entry: %s', line.name)
self.pool['ir.model.data'].unlink(cr, uid, data_ids, context=context)
return self.write(cr, uid, ids, {'purged': True}, context=context)
class CleanupPurgeWizardData(orm.TransientModel):
_inherit = 'cleanup.purge.wizard'
_name = 'cleanup.purge.wizard.data'
def default_get(self, cr, uid, fields, context=None):
res = super(CleanupPurgeWizardData, self).default_get(
cr, uid, fields, context=context)
if 'name' in fields:
res['name'] = _('Purge data')
return res
def find(self, cr, uid, context=None):
"""
Collect all rows from ir_model_data that refer
to a nonexisting model, or to a nonexisting
row in the model's table.
"""
res = []
data_pool = self.pool['ir.model.data']
data_ids = []
unknown_models = []
cr.execute("""SELECT DISTINCT(model) FROM ir_model_data""")
for (model,) in cr.fetchall():
if not model:
continue
if not self.pool.get(model):
unknown_models.append(model)
continue
cr.execute(
"""
SELECT id FROM ir_model_data
WHERE model = %%s
AND res_id IS NOT NULL
AND NOT EXISTS (
SELECT id FROM %s WHERE id=ir_model_data.res_id)
""" % self.pool[model]._table, (model,))
data_ids += [data_row[0] for data_row in cr.fetchall()]
data_ids += data_pool.search(
cr, uid, [('model', 'in', unknown_models)], context=context)
for data in data_pool.browse(cr, uid, data_ids, context=context):
res.append((0, 0, {
'data_id': data.id,
'name': "%s.%s, object of type %s" % (
data.module, data.name, data.model)}))
if not res:
raise orm.except_orm(
_('Nothing to do'),
_('No orphaned data entries found'))
return res
_columns = {
'purge_line_ids': fields.one2many(
'cleanup.purge.line.data',
'wizard_id', 'Data to purge'),
}

81
database_cleanup/model/purge_menus.py

@ -1,81 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2015 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
from openerp.tools.translate import _
class CleanupPurgeLineMenu(orm.TransientModel):
_inherit = 'cleanup.purge.line'
_name = 'cleanup.purge.line.menu'
_columns = {
'wizard_id': fields.many2one(
'cleanup.purge.wizard.menu', 'Purge Wizard', readonly=True),
'menu_id': fields.many2one('ir.ui.menu', 'Menu entry'),
}
def purge(self, cr, uid, ids, context=None):
self.pool['ir.ui.menu'].unlink(
cr, uid,
[this.menu_id.id for this in self.browse(cr, uid, ids,
context=context)],
context=context)
return self.write(cr, uid, ids, {'purged': True}, context=context)
class CleanupPurgeWizardMenu(orm.TransientModel):
_inherit = 'cleanup.purge.wizard'
_name = 'cleanup.purge.wizard.menu'
def default_get(self, cr, uid, fields, context=None):
res = super(CleanupPurgeWizardMenu, self).default_get(
cr, uid, fields, context=context)
if 'name' in fields:
res['name'] = _('Purge menus')
return res
def find(self, cr, uid, context=None):
"""
Search for models that cannot be instantiated.
"""
res = []
for menu in self.pool['ir.ui.menu'].browse(
cr, uid, self.pool['ir.ui.menu'].search(
cr, uid, [], context=dict(
context or {}, active_test=False))):
if not menu.action or menu.action.type != 'ir.actions.act_window':
continue
if not self.pool.get(menu.action.res_model):
res.append((0, 0, {
'name': menu.complete_name,
'menu_id': menu.id,
}))
if not res:
raise orm.except_orm(
_('Nothing to do'),
_('No dangling menu entries found'))
return res
_columns = {
'purge_line_ids': fields.one2many(
'cleanup.purge.line.menu',
'wizard_id', 'Menus to purge'),
}

155
database_cleanup/model/purge_models.py

@ -1,155 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
from openerp.tools.translate import _
from openerp.addons.base.ir.ir_model import MODULE_UNINSTALL_FLAG
class IrModel(orm.Model):
_inherit = 'ir.model'
def _drop_table(self, cr, uid, ids, context=None):
# Allow to skip this step during model unlink
# The super method crashes if the model cannot be instantiated
if context and context.get('no_drop_table'):
return True
return super(IrModel, self)._drop_table(cr, uid, ids, context=context)
def _inherited_models(self, cr, uid, ids, field_name, arg, context=None):
"""this function crashes for undefined models"""
result = dict((i, []) for i in ids)
existing_model_ids = [
this.id for this in self.browse(cr, uid, ids, context=context)
if self.pool.get(this.model)
]
super_result = super(IrModel, self)._inherited_models(
cr, uid, existing_model_ids, field_name, arg, context=context)
result.update(super_result)
return result
def _register_hook(self, cr):
# patch the function field instead of overwriting it
if self._columns['inherited_model_ids']._fnct !=\
self._inherited_models.__func__:
self._columns['inherited_model_ids']._fnct =\
self._inherited_models.__func__
return super(IrModel, self)._register_hook(cr)
class CleanupPurgeLineModel(orm.TransientModel):
_inherit = 'cleanup.purge.line'
_name = 'cleanup.purge.line.model'
_columns = {
'wizard_id': fields.many2one(
'cleanup.purge.wizard.model', 'Purge Wizard', readonly=True),
}
def purge(self, cr, uid, ids, context=None):
"""
Unlink models upon manual confirmation.
"""
model_pool = self.pool['ir.model']
attachment_pool = self.pool['ir.attachment']
constraint_pool = self.pool['ir.model.constraint']
fields_pool = self.pool['ir.model.fields']
relation_pool = self.pool['ir.model.relation']
local_context = (context or {}).copy()
local_context.update({
MODULE_UNINSTALL_FLAG: True,
'no_drop_table': True,
})
for line in self.browse(cr, uid, ids, context=context):
cr.execute(
"SELECT id, model from ir_model WHERE model = %s",
(line.name,))
row = cr.fetchone()
if row:
self.logger.info('Purging model %s', row[1])
attachment_ids = attachment_pool.search(
cr, uid, [('res_model', '=', line.name)], context=context)
if attachment_ids:
cr.execute(
"UPDATE ir_attachment SET res_model = FALSE "
"WHERE id in %s",
(tuple(attachment_ids), ))
constraint_ids = constraint_pool.search(
cr, uid, [('model', '=', line.name)], context=context)
if constraint_ids:
constraint_pool.unlink(
cr, uid, constraint_ids, context=context)
relation_ids = fields_pool.search(
cr, uid, [('relation', '=', row[1])], context=context)
for relation in relation_ids:
try:
# Fails if the model on the target side
# cannot be instantiated
fields_pool.unlink(cr, uid, [relation],
context=local_context)
except KeyError:
pass
except AttributeError:
pass
relation_ids = relation_pool.search(
cr, uid, [('model', '=', line.name)], context=context)
for relation in relation_ids:
relation_pool.unlink(cr, uid, [relation],
context=local_context)
model_pool.unlink(cr, uid, [row[0]], context=local_context)
line.write({'purged': True})
cr.commit()
return True
class CleanupPurgeWizardModel(orm.TransientModel):
_inherit = 'cleanup.purge.wizard'
_name = 'cleanup.purge.wizard.model'
def default_get(self, cr, uid, fields, context=None):
res = super(CleanupPurgeWizardModel, self).default_get(
cr, uid, fields, context=context)
if 'name' in fields:
res['name'] = _('Purge models')
return res
def find(self, cr, uid, context=None):
"""
Search for models that cannot be instantiated.
"""
res = []
cr.execute("SELECT model from ir_model")
for (model,) in cr.fetchall():
if not self.pool.get(model):
res.append((0, 0, {'name': model}))
if not res:
raise orm.except_orm(
_('Nothing to do'),
_('No orphaned models found'))
return res
_columns = {
'purge_line_ids': fields.one2many(
'cleanup.purge.line.model',
'wizard_id', 'Models to purge'),
}

129
database_cleanup/model/purge_modules.py

@ -1,129 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import pooler
from openerp.osv import orm, fields
from openerp.modules.module import get_module_path
from openerp.tools.translate import _
from openerp.addons.base.ir.ir_model import MODULE_UNINSTALL_FLAG
class IrModelConstraint(orm.Model):
_inherit = 'ir.model.constraint'
def _module_data_uninstall(self, cr, uid, ids, context=None):
"""this function crashes for constraints on undefined models"""
for this in self.browse(cr, uid, ids, context=context):
if not self.pool.get(this.model.model):
ids.remove(this.id)
this.unlink()
return super(IrModelConstraint, self)._module_data_uninstall(
cr, uid, ids, context=context)
class IrModelData(orm.Model):
_inherit = 'ir.model.data'
def _module_data_uninstall(self, cr, uid, modules_to_remove, context=None):
"""this function crashes for xmlids on undefined models or fields
referring to undefined models"""
if context is None:
context = {}
ids = self.search(cr, uid, [('module', 'in', modules_to_remove)])
for this in self.browse(cr, uid, ids, context=context):
if this.model == 'ir.model.fields':
ctx = context.copy()
ctx[MODULE_UNINSTALL_FLAG] = True
field = self.pool[this.model].browse(
cr, uid, this.res_id, context=ctx)
if not self.pool.get(field.model):
this.unlink()
continue
if not self.pool.get(this.model):
this.unlink()
return super(IrModelData, self)._module_data_uninstall(
cr, uid, modules_to_remove, context=context)
class CleanupPurgeLineModule(orm.TransientModel):
_inherit = 'cleanup.purge.line'
_name = 'cleanup.purge.line.module'
_columns = {
'wizard_id': fields.many2one(
'cleanup.purge.wizard.module', 'Purge Wizard', readonly=True),
}
def purge(self, cr, uid, ids, context=None):
"""
Uninstall modules upon manual confirmation, then reload
the database.
"""
module_pool = self.pool['ir.module.module']
lines = self.browse(cr, uid, ids, context=context)
module_names = [line.name for line in lines if not line.purged]
module_ids = module_pool.search(
cr, uid, [('name', 'in', module_names)], context=context)
if not module_ids:
return True
self.logger.info('Purging modules %s', ', '.join(module_names))
module_pool.write(
cr, uid, module_ids, {'state': 'to remove'}, context=context)
cr.commit()
_db, _pool = pooler.restart_pool(cr.dbname, update_module=True)
module_pool.unlink(cr, uid, module_ids, context=context)
return self.write(cr, uid, ids, {'purged': True}, context=context)
class CleanupPurgeWizardModule(orm.TransientModel):
_inherit = 'cleanup.purge.wizard'
_name = 'cleanup.purge.wizard.module'
def default_get(self, cr, uid, fields, context=None):
res = super(CleanupPurgeWizardModule, self).default_get(
cr, uid, fields, context=context)
if 'name' in fields:
res['name'] = _('Purge modules')
return res
def find(self, cr, uid, context=None):
module_pool = self.pool['ir.module.module']
module_ids = module_pool.search(cr, uid, [], context=context)
res = []
for module in module_pool.browse(cr, uid, module_ids, context=context):
if get_module_path(module.name):
continue
if module.state == 'uninstalled':
module_pool.unlink(cr, uid, module.id, context=context)
continue
res.append((0, 0, {'name': module.name}))
if not res:
raise orm.except_orm(
_('Nothing to do'),
_('No modules found to purge'))
return res
_columns = {
'purge_line_ids': fields.one2many(
'cleanup.purge.line.module',
'wizard_id', 'Modules to purge'),
}

87
database_cleanup/model/purge_wizard.py

@ -1,87 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
from openerp.osv import orm, fields
class CleanupPurgeLine(orm.AbstractModel):
""" Abstract base class for the purge wizard lines """
_name = 'cleanup.purge.line'
_order = 'name'
_columns = {
'name': fields.char('Name', size=256, readonly=True),
'purged': fields.boolean('Purged', readonly=True),
}
logger = logging.getLogger('openerp.addons.database_cleanup')
def purge(self, cr, uid, ids, context=None):
raise NotImplementedError
class PurgeWizard(orm.AbstractModel):
""" Abstract base class for the purge wizards """
_name = 'cleanup.purge.wizard'
def default_get(self, cr, uid, fields, context=None):
res = super(PurgeWizard, self).default_get(
cr, uid, fields, context=context)
if 'purge_line_ids' in fields:
res['purge_line_ids'] = self.find(cr, uid, context=None)
return res
def find(self, cr, uid, ids, context=None):
raise NotImplementedError
def purge_all(self, cr, uid, ids, context=None):
line_pool = self.pool[self._columns['purge_line_ids']._obj]
for wizard in self.browse(cr, uid, ids, context=context):
line_pool.purge(
cr, uid, [line.id for line in wizard.purge_line_ids],
context=context)
return True
def get_wizard_action(self, cr, uid, context=None):
wizard_id = self.create(cr, uid, {}, context=context)
return {
'type': 'ir.actions.act_window',
'views': [(False, 'form')],
'res_model': self._name,
'res_id': wizard_id,
'flags': {
'action_buttons': False,
'sidebar': False,
},
}
def select_lines(self, cr, uid, ids, context=None):
return {
'type': 'ir.actions.act_window',
'name': 'Select lines to purge',
'views': [(False, 'tree'), (False, 'form')],
'res_model': self._columns['purge_line_ids']._obj,
'domain': [('wizard_id', 'in', ids)],
}
_columns = {
'name': fields.char('Name', size=64, readonly=True),
}

0
database_cleanup/model/__init__.py → database_cleanup/models/__init__.py

123
database_cleanup/models/purge_columns.py

@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
# © 2014-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import _, api, fields, models
from openerp.exceptions import UserError
from openerp.osv import fields as legacy_fields
class CleanupPurgeLineColumn(models.TransientModel):
_inherit = 'cleanup.purge.line'
_name = 'cleanup.purge.line.column'
model_id = fields.Many2one('ir.model', 'Model', required=True,
ondelete='CASCADE')
wizard_id = fields.Many2one(
'cleanup.purge.wizard.column', 'Purge Wizard', readonly=True)
@api.multi
def purge(self):
"""
Unlink columns upon manual confirmation.
"""
for line in self:
if line.purged:
continue
model_pool = self.env[line.model_id.model]
# Check whether the column actually still exists.
# Inheritance such as stock.picking.in from stock.picking
# can lead to double attempts at removal
self.env.cr.execute(
'SELECT count(attname) FROM pg_attribute '
'WHERE attrelid = '
'( SELECT oid FROM pg_class WHERE relname = %s ) '
'AND attname = %s',
(model_pool._table, line.name))
if not self.env.cr.fetchone()[0]:
continue
self.logger.info(
'Dropping column %s from table %s',
line.name, model_pool._table)
self.env.cr.execute(
"""
ALTER TABLE "%s" DROP COLUMN "%s"
""" % (model_pool._table, line.name))
line.write({'purged': True})
self.env.cr.commit()
return True
class CleanupPurgeWizardColumn(models.TransientModel):
_inherit = 'cleanup.purge.wizard'
_name = 'cleanup.purge.wizard.column'
_description = 'Purge columns'
# List of known columns in use without corresponding fields
# Format: {table: [fields]}
blacklist = {
'wkf_instance': ['uid'], # lp:1277899
}
@api.model
def get_orphaned_columns(self, model_pools):
"""
From openobject-server/openerp/osv/orm.py
Iterate on the database columns to identify columns
of fields which have been removed
"""
columns = list(set([
column
for model_pool in model_pools
for column in model_pool._columns
if not (isinstance(model_pool._columns[column],
legacy_fields.function) and
not model_pool._columns[column].store)
]))
columns += models.MAGIC_COLUMNS
columns += self.blacklist.get(model_pools[0]._table, [])
self.env.cr.execute(
"SELECT a.attname FROM pg_class c, pg_attribute a "
"WHERE c.relname=%s AND c.oid=a.attrelid AND a.attisdropped=False "
"AND pg_catalog.format_type(a.atttypid, a.atttypmod) "
"NOT IN ('cid', 'tid', 'oid', 'xid') "
"AND a.attname NOT IN %s",
(model_pools[0]._table, tuple(columns)))
return [column for column, in self.env.cr.fetchall()]
@api.model
def find(self):
"""
Search for columns that are not in the corresponding model.
Group models by table to prevent false positives for columns
that are only in some of the models sharing the same table.
Example of this is 'sale_id' not being a field of stock.picking.in
"""
res = []
# mapping of tables to tuples (model id, [pool1, pool2, ...])
table2model = {}
for model in self.env['ir.model'].search([]):
if model.model not in self.env:
continue
model_pool = self.env[model.model]
if not model_pool._auto:
continue
table2model.setdefault(
model_pool._table, (model.id, [])
)[1].append(model_pool)
for table, model_spec in table2model.iteritems():
for column in self.get_orphaned_columns(model_spec[1]):
res.append((0, 0, {
'name': column,
'model_id': model_spec[0]}))
if not res:
raise UserError(_('No orphaned columns found'))
return res
purge_line_ids = fields.One2many(
'cleanup.purge.line.column', 'wizard_id', 'Columns to purge')

71
database_cleanup/models/purge_data.py

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# © 2014-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import _, api, fields, models
from openerp.exceptions import UserError
class CleanupPurgeLineData(models.TransientModel):
_inherit = 'cleanup.purge.line'
_name = 'cleanup.purge.line.data'
data_id = fields.Many2one('ir.model.data', 'Data entry')
wizard_id = fields.Many2one(
'cleanup.purge.wizard.data', 'Purge Wizard', readonly=True)
@api.multi
def purge(self):
"""
Unlink data entries upon manual confirmation.
"""
to_unlink = self.filtered(lambda x: not x.purged and x.data_id)
self.logger.info('Purging data entries: %s', to_unlink.mapped('name'))
to_unlink.mapped('data_id').unlink()
return self.write({'purged': True})
class CleanupPurgeWizardData(models.TransientModel):
_inherit = 'cleanup.purge.wizard'
_name = 'cleanup.purge.wizard.data'
_description = 'Purge data'
@api.model
def find(self):
"""
Collect all rows from ir_model_data that refer
to a nonexisting model, or to a nonexisting
row in the model's table.
"""
res = []
data_ids = []
unknown_models = []
self.env.cr.execute("""SELECT DISTINCT(model) FROM ir_model_data""")
for model, in self.env.cr.fetchall():
if not model:
continue
if model not in self.env:
unknown_models.append(model)
continue
self.env.cr.execute(
"""
SELECT id FROM ir_model_data
WHERE model = %%s
AND res_id IS NOT NULL
AND NOT EXISTS (
SELECT id FROM %s WHERE id=ir_model_data.res_id)
""" % self.env[model]._table, (model,))
data_ids.extend(data_row for data_row, in self.env.cr.fetchall())
data_ids += self.env['ir.model.data'].search([
('model', 'in', unknown_models),
]).ids
for data in self.env['ir.model.data'].browse(data_ids):
res.append((0, 0, {
'data_id': data.id,
'name': "%s.%s, object of type %s" % (
data.module, data.name, data.model)}))
if not res:
raise UserError(_('No orphaned data entries found'))
return res
purge_line_ids = fields.One2many(
'cleanup.purge.line.data', 'wizard_id', 'Data to purge')

48
database_cleanup/models/purge_menus.py

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# © 2014-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import _, api, fields, models
from openerp.exceptions import UserError
class CleanupPurgeLineMenu(models.TransientModel):
_inherit = 'cleanup.purge.line'
_name = 'cleanup.purge.line.menu'
wizard_id = fields.Many2one(
'cleanup.purge.wizard.menu', 'Purge Wizard', readonly=True)
menu_id = fields.Many2one('ir.ui.menu', 'Menu entry')
@api.multi
def purge(self):
self.mapped('menu_id').unlink()
return self.write({'purged': True})
class CleanupPurgeWizardMenu(models.TransientModel):
_inherit = 'cleanup.purge.wizard'
_name = 'cleanup.purge.wizard.menu'
_description = 'Purge menus'
@api.model
def find(self):
"""
Search for models that cannot be instantiated.
"""
res = []
for menu in self.env['ir.ui.menu'].with_context(active_test=False)\
.search([('action', '!=', False)]):
if menu.action.type != 'ir.actions.act_window':
continue
if menu.action.res_model not in self.env or\
menu.action.src_model not in self.env:
res.append((0, 0, {
'name': menu.complete_name,
'menu_id': menu.id,
}))
if not res:
raise UserError(_('No dangling menu entries found'))
return res
purge_line_ids = fields.One2many(
'cleanup.purge.line.menu', 'wizard_id', 'Menus to purge')

120
database_cleanup/models/purge_models.py

@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
# © 2014-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import _, api, models, fields
from openerp.exceptions import UserError
from openerp.addons.base.ir.ir_model import MODULE_UNINSTALL_FLAG
class IrModel(models.Model):
_inherit = 'ir.model'
@api.multi
def _drop_table(self):
# Allow to skip this step during model unlink
# The super method crashes if the model cannot be instantiated
if self.env.context.get('no_drop_table'):
return True
return super(IrModel, self)._drop_table()
@api.multi
def _inherited_models(self, field_name, arg):
"""this function crashes for undefined models"""
result = dict((i, []) for i in self.ids)
existing_model_ids = [
this.id for this in self if this.model in self.env
]
super_result = super(IrModel, self.browse(existing_model_ids))\
._inherited_models(field_name, arg)
result.update(super_result)
return result
def _register_hook(self, cr):
# patch the function field instead of overwriting it
if self._columns['inherited_model_ids']._fnct !=\
self._inherited_models.__func__:
self._columns['inherited_model_ids']._fnct =\
self._inherited_models.__func__
return super(IrModel, self)._register_hook(cr)
class CleanupPurgeLineModel(models.TransientModel):
_inherit = 'cleanup.purge.line'
_name = 'cleanup.purge.line.model'
_description = 'Purge models'
wizard_id = fields.Many2one(
'cleanup.purge.wizard.model', 'Purge Wizard', readonly=True)
@api.multi
def purge(self):
"""
Unlink models upon manual confirmation.
"""
context_flags = {
MODULE_UNINSTALL_FLAG: True,
'no_drop_table': True,
}
for line in self:
self.env.cr.execute(
"SELECT id, model from ir_model WHERE model = %s",
(line.name,))
row = self.env.cr.fetchone()
if not row:
continue
self.logger.info('Purging model %s', row[1])
attachments = self.env['ir.attachment'].search([
('res_model', '=', line.name)
])
if attachments:
self.env.cr.execute(
"UPDATE ir_attachment SET res_model = NULL "
"WHERE id in %s",
(tuple(attachments.ids), ))
self.env['ir.model.constraint'].search([
('model', '=', line.name),
]).unlink()
relations = self.env['ir.model.fields'].search([
('relation', '=', row[1]),
]).with_context(**context_flags)
for relation in relations:
try:
# Fails if the model on the target side
# cannot be instantiated
relation.unlink()
except KeyError:
pass
except AttributeError:
pass
self.env['ir.model.relation'].search([
('model', '=', line.name)
]).with_context(**context_flags).unlink()
self.env['ir.model'].browse([row[0]])\
.with_context(**context_flags).unlink()
line.write({'purged': True})
self.env.cr.commit()
return True
class CleanupPurgeWizardModel(models.TransientModel):
_inherit = 'cleanup.purge.wizard'
_name = 'cleanup.purge.wizard.model'
_description = 'Purge models'
@api.model
def find(self):
"""
Search for models that cannot be instantiated.
"""
res = []
self.env.cr.execute("SELECT model from ir_model")
for model, in self.env.cr.fetchall():
if model not in self.env:
res.append((0, 0, {'name': model}))
if not res:
raise UserError(_('No orphaned models found'))
return res
purge_line_ids = fields.One2many(
'cleanup.purge.line.model', 'wizard_id', 'Models to purge')

79
database_cleanup/models/purge_modules.py

@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
# © 2014-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import _, api, fields, models
from openerp.exceptions import UserError
from openerp.modules.registry import RegistryManager
from openerp.modules.module import get_module_path
from openerp.addons.base.ir.ir_model import MODULE_UNINSTALL_FLAG
class IrModelData(models.Model):
_inherit = 'ir.model.data'
@api.model
def _module_data_uninstall(self, modules_to_remove):
"""this function crashes for xmlids on undefined models or fields
referring to undefined models"""
for this in self.search([('module', 'in', modules_to_remove)]):
if this.model == 'ir.model.fields':
field = self.env[this.model].with_context(
**{MODULE_UNINSTALL_FLAG: True}).browse(this.res_id)
if field.model not in self.env:
this.unlink()
continue
if this.model not in self.env:
this.unlink()
return super(IrModelData, self)._module_data_uninstall(
modules_to_remove)
class CleanupPurgeLineModule(models.TransientModel):
_inherit = 'cleanup.purge.line'
_name = 'cleanup.purge.line.module'
wizard_id = fields.Many2one(
'cleanup.purge.wizard.module', 'Purge Wizard', readonly=True)
@api.multi
def purge(self):
"""
Uninstall modules upon manual confirmation, then reload
the database.
"""
module_names = self.filtered(lambda x: not x.purged).mapped('name')
modules = self.env['ir.module.module'].search([
('name', 'in', module_names)
])
if not modules:
return True
self.logger.info('Purging modules %s', ', '.join(module_names))
modules.write({'state': 'to remove'})
self.env.cr.commit()
RegistryManager.new(self.env.cr.dbname, update_module=True)
modules.unlink()
return self.write({'purged': True})
class CleanupPurgeWizardModule(models.TransientModel):
_inherit = 'cleanup.purge.wizard'
_name = 'cleanup.purge.wizard.module'
_description = 'Purge modules'
@api.model
def find(self):
res = []
for module in self.env['ir.module.module'].search([]):
if get_module_path(module.name):
continue
if module.state == 'uninstalled':
module.unlink()
continue
res.append((0, 0, {'name': module.name}))
if not res:
raise UserError(_('No modules found to purge'))
return res
purge_line_ids = fields.One2many(
'cleanup.purge.line.module', 'wizard_id', 'Modules to purge')

89
database_cleanup/model/purge_tables.py → database_cleanup/models/purge_tables.py

@ -1,44 +1,24 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
# © 2014-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import api, fields, models, _
from openerp.exceptions import UserError
from openerp.osv import orm, fields
from openerp.tools.translate import _
class CleanupPurgeLineTable(orm.TransientModel):
class CleanupPurgeLineTable(models.TransientModel):
_inherit = 'cleanup.purge.line' _inherit = 'cleanup.purge.line'
_name = 'cleanup.purge.line.table' _name = 'cleanup.purge.line.table'
_columns = {
'wizard_id': fields.many2one(
'cleanup.purge.wizard.table', 'Purge Wizard', readonly=True),
}
wizard_id = fields.Many2one(
'cleanup.purge.wizard.table', 'Purge Wizard', readonly=True)
def purge(self, cr, uid, ids, context=None):
@api.multi
def purge(self):
""" """
Unlink tables upon manual confirmation. Unlink tables upon manual confirmation.
""" """
lines = self.browse(cr, uid, ids, context=context)
tables = [line.name for line in lines]
for line in lines:
tables = self.mapped('name')
for line in self:
if line.purged: if line.purged:
continue continue
@ -49,7 +29,7 @@ class CleanupPurgeLineTable(orm.TransientModel):
# message-id/22895.1226088573@sss.pgh.pa.us # message-id/22895.1226088573@sss.pgh.pa.us
# Only using the constraint name and the source table, # Only using the constraint name and the source table,
# but I'm leaving the rest in for easier debugging # but I'm leaving the rest in for easier debugging
cr.execute(
self.env.cr.execute(
""" """
SELECT conname, confrelid::regclass, af.attname AS fcol, SELECT conname, confrelid::regclass, af.attname AS fcol,
conrelid::regclass, a.attname AS col conrelid::regclass, a.attname AS col
@ -64,48 +44,40 @@ class CleanupPurgeLineTable(orm.TransientModel):
AND confrelid::regclass = '%s'::regclass; AND confrelid::regclass = '%s'::regclass;
""" % line.name) """ % line.name)
for constraint in cr.fetchall():
for constraint in self.env.cr.fetchall():
if constraint[3] in tables: if constraint[3] in tables:
self.logger.info( self.logger.info(
'Dropping constraint %s on table %s (to be dropped)', 'Dropping constraint %s on table %s (to be dropped)',
constraint[0], constraint[3]) constraint[0], constraint[3])
cr.execute(
self.env.cr.execute(
"ALTER TABLE %s DROP CONSTRAINT %s" % ( "ALTER TABLE %s DROP CONSTRAINT %s" % (
constraint[3], constraint[0])) constraint[3], constraint[0]))
self.logger.info( self.logger.info(
'Dropping table %s', line.name) 'Dropping table %s', line.name)
cr.execute("DROP TABLE \"%s\"" % (line.name,))
self.env.cr.execute("DROP TABLE \"%s\"" % (line.name,))
line.write({'purged': True}) line.write({'purged': True})
cr.commit()
self.env.cr.commit()
return True return True
class CleanupPurgeWizardTable(orm.TransientModel):
class CleanupPurgeWizardTable(models.TransientModel):
_inherit = 'cleanup.purge.wizard' _inherit = 'cleanup.purge.wizard'
_name = 'cleanup.purge.wizard.table' _name = 'cleanup.purge.wizard.table'
_description = 'Purge tables'
def default_get(self, cr, uid, fields, context=None):
res = super(CleanupPurgeWizardTable, self).default_get(
cr, uid, fields, context=context)
if 'name' in fields:
res['name'] = _('Purge tables')
return res
def find(self, cr, uid, context=None):
@api.model
def find(self):
""" """
Search for tables that cannot be instantiated. Search for tables that cannot be instantiated.
Ignore views for now. Ignore views for now.
""" """
model_ids = self.pool['ir.model'].search(cr, uid, [], context=context)
# Start out with known tables with no model # Start out with known tables with no model
known_tables = ['wkf_witm_trans'] known_tables = ['wkf_witm_trans']
for model in self.pool['ir.model'].browse(
cr, uid, model_ids, context=context):
model_pool = self.pool.get(model.model)
if not model_pool:
for model in self.env['ir.model'].search([]):
if model.model not in self.env:
continue continue
model_pool = self.env[model.model]
known_tables.append(model_pool._table) known_tables.append(model_pool._table)
known_tables += [ known_tables += [
column._sql_names(model_pool)[0] column._sql_names(model_pool)[0]
@ -118,21 +90,16 @@ class CleanupPurgeWizardTable(orm.TransientModel):
# Cannot pass table names as a psycopg argument # Cannot pass table names as a psycopg argument
known_tables_repr = ",".join( known_tables_repr = ",".join(
[("'%s'" % table) for table in known_tables]) [("'%s'" % table) for table in known_tables])
cr.execute(
self.env.cr.execute(
""" """
SELECT table_name FROM information_schema.tables SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_type = 'BASE TABLE' WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
AND table_name NOT IN (%s)""" % known_tables_repr) AND table_name NOT IN (%s)""" % known_tables_repr)
res = [(0, 0, {'name': row[0]}) for row in cr.fetchall()]
res = [(0, 0, {'name': row[0]}) for row in self.env.cr.fetchall()]
if not res: if not res:
raise orm.except_orm(
_('Nothing to do'),
_('No orphaned tables found'))
raise UserError(_('No orphaned tables found'))
return res return res
_columns = {
'purge_line_ids': fields.one2many(
'cleanup.purge.line.table',
'wizard_id', 'Tables to purge'),
}
purge_line_ids = fields.One2many(
'cleanup.purge.line.table', 'wizard_id', 'Tables to purge')

77
database_cleanup/models/purge_wizard.py

@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
# © 2014-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
from openerp import _, api, fields, models
class CleanupPurgeLine(models.AbstractModel):
""" Abstract base class for the purge wizard lines """
_name = 'cleanup.purge.line'
_order = 'name'
name = fields.Char('Name', readonly=True)
purged = fields.Boolean('Purged', readonly=True)
wizard_id = fields.Many2one('cleanup.purge.wizard')
logger = logging.getLogger('openerp.addons.database_cleanup')
@api.multi
def purge(self):
raise NotImplementedError
class PurgeWizard(models.AbstractModel):
""" Abstract base class for the purge wizards """
_name = 'cleanup.purge.wizard'
_description = 'Purge stuff'
@api.model
def default_get(self, fields_list):
res = super(PurgeWizard, self).default_get(fields_list)
if 'purge_line_ids' in fields_list:
res['purge_line_ids'] = self.find()
return res
@api.multi
def find(self):
raise NotImplementedError
@api.multi
def purge_all(self):
self.mapped('purge_line_ids').purge()
return True
@api.model
def get_wizard_action(self):
wizard = self.create({})
return {
'type': 'ir.actions.act_window',
'name': wizard.display_name,
'views': [(False, 'form')],
'res_model': self._name,
'res_id': wizard.id,
'flags': {
'action_buttons': False,
'sidebar': False,
},
}
@api.multi
def select_lines(self):
return {
'type': 'ir.actions.act_window',
'name': _('Select lines to purge'),
'views': [(False, 'tree'), (False, 'form')],
'res_model': self._fields['purge_line_ids'].comodel_name,
'domain': [('wizard_id', 'in', self.ids)],
}
@api.multi
def name_get(self):
return [
(this.id, self._description)
for this in self
]
purge_line_ids = fields.One2many('cleanup.purge.line', 'wizard_id')

4
database_cleanup/tests/__init__.py

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# © 2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import test_database_cleanup

50
database_cleanup/tests/test_database_cleanup.py

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# © 2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from psycopg2 import ProgrammingError
from openerp.tests.common import TransactionCase
class TestDatabaseCleanup(TransactionCase):
def test_database_cleanup(self):
# create an orphaned column
self.cr.execute(
'alter table res_users add column database_cleanup_test int')
purge_columns = self.env['cleanup.purge.wizard.column'].create({})
purge_columns.purge_all()
# must be removed by the wizard
with self.assertRaises(ProgrammingError):
with self.registry.cursor() as cr:
cr.execute('select database_cleanup_test from res_users')
# create a data entry pointing nowhere
self.cr.execute('select max(id) + 1 from res_users')
self.env['ir.model.data'].create({
'module': 'database_cleanup',
'name': 'test_no_data_entry',
'model': 'res.users',
'res_id': self.cr.fetchone()[0],
})
purge_data = self.env['cleanup.purge.wizard.data'].create({})
purge_data.purge_all()
# must be removed by the wizard
with self.assertRaises(ValueError):
self.env.ref('database_cleanup.test_no_data_entry')
# create a nonexistent model
self.env['ir.model'].create({
'name': 'Database cleanup test model',
'model': 'x_database.cleanup.test.model',
})
self.env.cr.execute(
'insert into ir_attachment (name, res_model, res_id, type) values '
"('test attachment', 'database.cleanup.test.model', 42, 'binary')")
self.registry.models.pop('x_database.cleanup.test.model')
self.registry._pure_function_fields.pop(
'x_database.cleanup.test.model')
purge_models = self.env['cleanup.purge.wizard.model'].create({})
purge_models.purge_all()
# must be removed by the wizard
self.assertFalse(self.env['ir.model'].search([
('model', '=', 'x_database.cleanup.test.model'),
]))

0
database_cleanup/view/menu.xml → database_cleanup/views/menu.xml

35
database_cleanup/view/purge_columns.xml → database_cleanup/views/purge_columns.xml

@ -1,32 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp> <openerp>
<data> <data>
<record id="purge_columns_view" model="ir.ui.view"> <record id="purge_columns_view" model="ir.ui.view">
<field name="name">Form view for purge columns wizard</field>
<field name="model">cleanup.purge.wizard.column</field> <field name="model">cleanup.purge.wizard.column</field>
<field name="inherit_id" ref="form_purge_wizard" />
<field name="mode">primary</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Purge columns" version="7.0">
<h1>
<field name="name"/>
</h1>
<button type="object" name="purge_all" string="Purge all columns" />
<button type="object" name="select_lines" string="Select lines" />
<field name="purge_line_ids" colspan="4" nolabel="1">
<form string="Purge columns">
<group>
<field name="name" />
<field name="name" position="after">
<field name="model_id" /> <field name="model_id" />
<field name="purged" invisible="0" />
</group>
<footer>
<button type="object" name="purge"
icon="gtk-cancel" string="Purge this column"
attrs="{'invisible': [('purged', '=', True)]}"/>
</footer>
</form>
</field> </field>
</form>
</field> </field>
</record> </record>
@ -40,15 +22,12 @@
<record id="purge_column_line_tree" model="ir.ui.view"> <record id="purge_column_line_tree" model="ir.ui.view">
<field name="model">cleanup.purge.line.column</field> <field name="model">cleanup.purge.line.column</field>
<field name="inherit_id" ref="tree_purge_line" />
<field name="mode">primary</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Purge columns">
<field name="name" />
<field name="name" position="after">
<field name="model_id" /> <field name="model_id" />
<field name="purged" invisible="0" />
<button type="object" name="purge"
icon="gtk-cancel" string="Purge this column"
attrs="{'invisible': [('purged', '=', True)]}"/>
</tree>
</field>
</field> </field>
</record> </record>

35
database_cleanup/view/purge_data.xml → database_cleanup/views/purge_data.xml

@ -1,32 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp> <openerp>
<data> <data>
<record id="purge_data_view" model="ir.ui.view"> <record id="purge_data_view" model="ir.ui.view">
<field name="name">Form view for purge data wizard</field>
<field name="model">cleanup.purge.wizard.data</field> <field name="model">cleanup.purge.wizard.data</field>
<field name="inherit_id" ref="form_purge_wizard" />
<field name="mode">primary</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Purge data entries that refer to missing resources" version="7.0">
<h1>
<field name="name"/>
</h1>
<button type="object" name="purge_all" string="Purge all data" />
<button type="object" name="select_lines" string="Select lines" />
<field name="purge_line_ids" colspan="4" nolabel="1">
<form string="Purge data">
<group>
<field name="name" />
<field name="name" position="after">
<field name="data_id" /> <field name="data_id" />
<field name="purged" invisible="0" />
</group>
<footer>
<button type="object" name="purge"
icon="gtk-cancel" string="Purge this data"
attrs="{'invisible': [('purged', '=', True)]}"/>
</footer>
</form>
</field> </field>
</form>
</field> </field>
</record> </record>
@ -40,15 +22,12 @@
<record id="purge_data_line_tree" model="ir.ui.view"> <record id="purge_data_line_tree" model="ir.ui.view">
<field name="model">cleanup.purge.line.data</field> <field name="model">cleanup.purge.line.data</field>
<field name="inherit_id" ref="tree_purge_line" />
<field name="mode">primary</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Purge data">
<field name="name" />
<field name="name" position="after">
<field name="data_id" /> <field name="data_id" />
<field name="purged" invisible="0" />
<button type="object" name="purge"
icon="gtk-cancel" string="Purge this data"
attrs="{'invisible': [('purged', '=', True)]}"/>
</tree>
</field>
</field> </field>
</record> </record>

34
database_cleanup/view/purge_menus.xml → database_cleanup/views/purge_menus.xml

@ -2,29 +2,11 @@
<openerp> <openerp>
<data> <data>
<record id="purge_menus_view" model="ir.ui.view"> <record id="purge_menus_view" model="ir.ui.view">
<field name="name">Form view for purge menus wizard</field>
<field name="model">cleanup.purge.wizard.menu</field> <field name="model">cleanup.purge.wizard.menu</field>
<field name="inherit_id" ref="form_purge_wizard" />
<field name="mode">primary</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form>
<h1>
<field name="name"/>
</h1>
<button type="object" name="purge_all" string="Purge all menus" />
<button type="object" name="select_lines" string="Select lines" />
<field name="purge_line_ids">
<form>
<group>
<field name="name" />
<field name="purged" invisible="0" />
</group>
<footer>
<button type="object" name="purge"
icon="gtk-cancel" string="Purge this menu"
attrs="{'invisible': [('purged', '=', True)]}"/>
</footer>
</form>
</field>
</form>
<data/>
</field> </field>
</record> </record>
@ -38,14 +20,10 @@
<record id="purge_menu_line_tree" model="ir.ui.view"> <record id="purge_menu_line_tree" model="ir.ui.view">
<field name="model">cleanup.purge.line.menu</field> <field name="model">cleanup.purge.line.menu</field>
<field name="inherit_id" ref="tree_purge_line" />
<field name="mode">primary</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree>
<field name="name" />
<field name="purged" invisible="0" />
<button type="object" name="purge"
icon="gtk-cancel" string="Purge this model"
attrs="{'invisible': [('purged', '=', True)]}"/>
</tree>
<data />
</field> </field>
</record> </record>

35
database_cleanup/view/purge_models.xml → database_cleanup/views/purge_models.xml

@ -1,31 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp> <openerp>
<data> <data>
<record id="purge_models_view" model="ir.ui.view"> <record id="purge_models_view" model="ir.ui.view">
<field name="name">Form view for purge models wizard</field>
<field name="model">cleanup.purge.wizard.model</field> <field name="model">cleanup.purge.wizard.model</field>
<field name="inherit_id" ref="form_purge_wizard" />
<field name="mode">primary</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Purge models" version="7.0">
<h1>
<field name="name"/>
</h1>
<button type="object" name="purge_all" string="Purge all models" />
<button type="object" name="select_lines" string="Select lines" />
<field name="purge_line_ids" colspan="4" nolabel="1">
<form string="Purge models">
<group>
<field name="name" />
<field name="purged" invisible="0" />
</group>
<footer>
<button type="object" name="purge"
icon="gtk-cancel" string="Purge this model"
attrs="{'invisible': [('purged', '=', True)]}"/>
</footer>
</form>
</field>
</form>
<data />
</field> </field>
</record> </record>
@ -39,14 +20,10 @@
<record id="purge_model_line_tree" model="ir.ui.view"> <record id="purge_model_line_tree" model="ir.ui.view">
<field name="model">cleanup.purge.line.model</field> <field name="model">cleanup.purge.line.model</field>
<field name="inherit_id" ref="tree_purge_line" />
<field name="mode">primary</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Purge models">
<field name="name" />
<field name="purged" invisible="0" />
<button type="object" name="purge"
icon="gtk-cancel" string="Purge this model"
attrs="{'invisible': [('purged', '=', True)]}"/>
</tree>
<data />
</field> </field>
</record> </record>

35
database_cleanup/view/purge_modules.xml → database_cleanup/views/purge_modules.xml

@ -1,31 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp> <openerp>
<data> <data>
<record id="purge_modules_view" model="ir.ui.view"> <record id="purge_modules_view" model="ir.ui.view">
<field name="name">Form view for purge modules wizard</field>
<field name="model">cleanup.purge.wizard.module</field> <field name="model">cleanup.purge.wizard.module</field>
<field name="inherit_id" ref="form_purge_wizard" />
<field name="mode">primary</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Purge modules" version="7.0">
<h1>
<field name="name"/>
</h1>
<button type="object" name="purge_all" string="Purge all modules" />
<button type="object" name="select_lines" string="Select lines" />
<field name="purge_line_ids" colspan="4" nolabel="1">
<form string="Purge modules">
<group>
<field name="name" />
<field name="purged" invisible="0" />
</group>
<footer>
<button type="object" name="purge"
icon="gtk-cancel" string="Purge this module"
attrs="{'invisible': [('purged', '=', True)]}"/>
</footer>
</form>
</field>
</form>
<data />
</field> </field>
</record> </record>
@ -39,14 +20,10 @@
<record id="purge_module_line_tree" model="ir.ui.view"> <record id="purge_module_line_tree" model="ir.ui.view">
<field name="model">cleanup.purge.line.module</field> <field name="model">cleanup.purge.line.module</field>
<field name="inherit_id" ref="tree_purge_line" />
<field name="mode">primary</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Purge modules">
<field name="name" />
<field name="purged" invisible="0" />
<button type="object" name="purge"
icon="gtk-cancel" string="Purge this module"
attrs="{'invisible': [('purged', '=', True)]}"/>
</tree>
<data/>
</field> </field>
</record> </record>

31
database_cleanup/view/purge_tables.xml → database_cleanup/views/purge_tables.xml

@ -1,27 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp> <openerp>
<data> <data>
<record id="purge_tables_view" model="ir.ui.view"> <record id="purge_tables_view" model="ir.ui.view">
<field name="name">Form view for purge tables wizard</field>
<field name="model">cleanup.purge.wizard.table</field> <field name="model">cleanup.purge.wizard.table</field>
<field name="inherit_id" ref="form_purge_wizard" />
<field name="mode">primary</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Purge tables" version="7.0">
<h1>
<field name="name"/>
</h1>
<button type="object" name="purge_all" string="Purge all tables" />
<button type="object" name="select_lines" string="Select lines" />
<field name="purge_line_ids" colspan="4" nolabel="1">
<tree string="Purge tables">
<field name="name" />
<field name="purged" invisible="0" />
<button type="object" name="purge"
icon="gtk-cancel" string="Purge this table"
attrs="{'invisible': [('purged', '=', True)]}"/>
</tree>
</field>
</form>
<data />
</field> </field>
</record> </record>
@ -35,14 +20,10 @@
<record id="purge_table_line_tree" model="ir.ui.view"> <record id="purge_table_line_tree" model="ir.ui.view">
<field name="model">cleanup.purge.line.table</field> <field name="model">cleanup.purge.line.table</field>
<field name="inherit_id" ref="tree_purge_line" />
<field name="mode">primary</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Purge tables">
<field name="name" />
<field name="purged" invisible="0" />
<button type="object" name="purge"
icon="gtk-cancel" string="Purge this table"
attrs="{'invisible': [('purged', '=', True)]}"/>
</tree>
<data />
</field> </field>
</record> </record>

41
database_cleanup/views/purge_wizard.xml

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="form_purge_wizard" model="ir.ui.view">
<field name="model">cleanup.purge.wizard</field>
<field name="arch" type="xml">
<form>
<header>
<button type="object" name="purge_all" string="Purge all" class="oe_highlight" />
<button type="object" name="select_lines" string="Select lines" />
</header>
<field name="purge_line_ids">
<form>
<group>
<field name="name" />
<field name="purged" />
</group>
<footer>
<button type="object" name="purge" class="oe_highlight"
string="Purge"
attrs="{'invisible': [('purged', '=', True)]}"/>
</footer>
</form>
</field>
</form>
</field>
</record>
<record id="tree_purge_line" model="ir.ui.view">
<field name="model">cleanup.purge.line</field>
<field name="arch" type="xml">
<tree string="Purge models" delete="false">
<field name="name" />
<field name="purged" />
<button type="object" name="purge"
icon="gtk-cancel" string="Purge this model"
attrs="{'invisible': [('purged', '=', True)]}"/>
</tree>
</field>
</record>
</data>
</openerp>
Loading…
Cancel
Save