From e294819ad4698b8c27ac4bf15e36f6d676594bef Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Tue, 30 Dec 2014 13:14:51 +0100 Subject: [PATCH] [ADD] database_cleanup: Module for cleaning up migrated databases --- database_cleanup/__init__.py | 1 + database_cleanup/__openerp__.py | 55 ++++++++ database_cleanup/model/__init__.py | 6 + database_cleanup/model/purge_columns.py | 154 +++++++++++++++++++++++ database_cleanup/model/purge_data.py | 106 ++++++++++++++++ database_cleanup/model/purge_models.py | 127 +++++++++++++++++++ database_cleanup/model/purge_modules.py | 91 ++++++++++++++ database_cleanup/model/purge_tables.py | 138 ++++++++++++++++++++ database_cleanup/model/purge_wizard.py | 64 ++++++++++ database_cleanup/static/src/img/icon.png | Bin 0 -> 30647 bytes database_cleanup/view/menu.xml | 48 +++++++ database_cleanup/view/purge_columns.xml | 37 ++++++ database_cleanup/view/purge_data.xml | 37 ++++++ database_cleanup/view/purge_models.xml | 36 ++++++ database_cleanup/view/purge_modules.xml | 36 ++++++ database_cleanup/view/purge_tables.xml | 36 ++++++ 16 files changed, 972 insertions(+) create mode 100644 database_cleanup/__init__.py create mode 100644 database_cleanup/__openerp__.py create mode 100644 database_cleanup/model/__init__.py create mode 100644 database_cleanup/model/purge_columns.py create mode 100644 database_cleanup/model/purge_data.py create mode 100644 database_cleanup/model/purge_models.py create mode 100644 database_cleanup/model/purge_modules.py create mode 100644 database_cleanup/model/purge_tables.py create mode 100644 database_cleanup/model/purge_wizard.py create mode 100644 database_cleanup/static/src/img/icon.png create mode 100644 database_cleanup/view/menu.xml create mode 100644 database_cleanup/view/purge_columns.xml create mode 100644 database_cleanup/view/purge_data.xml create mode 100644 database_cleanup/view/purge_models.xml create mode 100644 database_cleanup/view/purge_modules.xml create mode 100644 database_cleanup/view/purge_tables.xml diff --git a/database_cleanup/__init__.py b/database_cleanup/__init__.py new file mode 100644 index 000000000..9186ee3ad --- /dev/null +++ b/database_cleanup/__init__.py @@ -0,0 +1 @@ +from . import model diff --git a/database_cleanup/__openerp__.py b/database_cleanup/__openerp__.py new file mode 100644 index 000000000..12c723365 --- /dev/null +++ b/database_cleanup/__openerp__.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# 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 . +# +############################################################################## + +{ + 'name': 'Database cleanup', + 'version': '0.1', + 'author': 'Therp BV', + 'depends': ['base'], + 'license': 'AGPL-3', + 'category': 'Tools', + 'data': [ + 'view/purge_modules.xml', + 'view/purge_models.xml', + 'view/purge_columns.xml', + 'view/purge_tables.xml', + 'view/purge_data.xml', + 'view/menu.xml', + ], + 'description': """\ +Clean your OpenERP database from remnants of modules, models, columns and +tables left by uninstalled modules (prior to 7.0) or a homebrew database +upgrade to a new major version of OpenERP. + +After installation of this module, go to the Settings menu -> Technical -> +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 +in your database. You can either delete entries by line, or sweep all entries +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. +""", + +} diff --git a/database_cleanup/model/__init__.py b/database_cleanup/model/__init__.py new file mode 100644 index 000000000..77faf891c --- /dev/null +++ b/database_cleanup/model/__init__.py @@ -0,0 +1,6 @@ +from . import purge_wizard +from . import purge_modules +from . import purge_models +from . import purge_columns +from . import purge_tables +from . import purge_data diff --git a/database_cleanup/model/purge_columns.py b/database_cleanup/model/purge_columns.py new file mode 100644 index 000000000..60b56ccc8 --- /dev/null +++ b/database_cleanup/model/purge_columns.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# 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 . +# +############################################################################## + +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'), + } diff --git a/database_cleanup/model/purge_data.py b/database_cleanup/model/purge_data.py new file mode 100644 index 000000000..bcab2d832 --- /dev/null +++ b/database_cleanup/model/purge_data.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# 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 . +# +############################################################################## + +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 res_id NOT IN ( + SELECT id FROM %s) + """ % 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'), + } diff --git a/database_cleanup/model/purge_models.py b/database_cleanup/model/purge_models.py new file mode 100644 index 000000000..a6342afd3 --- /dev/null +++ b/database_cleanup/model/purge_models.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# 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 . +# +############################################################################## + +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) + + +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'] + + 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 AttributeError: + pass + 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'), + } diff --git a/database_cleanup/model/purge_modules.py b/database_cleanup/model/purge_modules.py new file mode 100644 index 000000000..12734d157 --- /dev/null +++ b/database_cleanup/model/purge_modules.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# 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 . +# +############################################################################## + +from openerp import pooler +from openerp.osv import orm, fields +from openerp.modules.module import get_module_path +from openerp.tools.translate import _ + + +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'), + } diff --git a/database_cleanup/model/purge_tables.py b/database_cleanup/model/purge_tables.py new file mode 100644 index 000000000..8d9c86544 --- /dev/null +++ b/database_cleanup/model/purge_tables.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# 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 . +# +############################################################################## + +from openerp.osv import orm, fields +from openerp.tools.translate import _ + + +class CleanupPurgeLineTable(orm.TransientModel): + _inherit = 'cleanup.purge.line' + _name = 'cleanup.purge.line.table' + + _columns = { + 'wizard_id': fields.many2one( + 'cleanup.purge.wizard.table', 'Purge Wizard', readonly=True), + } + + def purge(self, cr, uid, ids, context=None): + """ + Unlink tables upon manual confirmation. + """ + lines = self.browse(cr, uid, ids, context=context) + tables = [line.name for line in lines] + for line in lines: + if line.purged: + continue + + # Retrieve constraints on the tables to be dropped + # This query is referenced in numerous places + # on the Internet but credits probably go to Tom Lane + # in this post http://www.postgresql.org/\ + # message-id/22895.1226088573@sss.pgh.pa.us + # Only using the constraint name and the source table, + # but I'm leaving the rest in for easier debugging + cr.execute( + """ + SELECT conname, confrelid::regclass, af.attname AS fcol, + conrelid::regclass, a.attname AS col + FROM pg_attribute af, pg_attribute a, + (SELECT conname, conrelid, confrelid,conkey[i] AS conkey, + confkey[i] AS confkey + FROM (select conname, conrelid, confrelid, conkey, + confkey, generate_series(1,array_upper(conkey,1)) AS i + FROM pg_constraint WHERE contype = 'f') ss) ss2 + WHERE af.attnum = confkey AND af.attrelid = confrelid AND + a.attnum = conkey AND a.attrelid = conrelid + AND confrelid::regclass = '%s'::regclass; + """ % line.name) + + for constraint in cr.fetchall(): + if constraint[3] in tables: + self.logger.info( + 'Dropping constraint %s on table %s (to be dropped)', + constraint[0], constraint[3]) + cr.execute( + "ALTER TABLE %s DROP CONSTRAINT %s" % ( + constraint[3], constraint[0])) + + self.logger.info( + 'Dropping table %s', line.name) + cr.execute("DROP TABLE \"%s\"" % (line.name,)) + line.write({'purged': True}) + cr.commit() + return True + + +class CleanupPurgeWizardTable(orm.TransientModel): + _inherit = 'cleanup.purge.wizard' + _name = 'cleanup.purge.wizard.table' + + 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): + """ + Search for tables that cannot be instantiated. + Ignore views for now. + """ + model_ids = self.pool['ir.model'].search(cr, uid, [], context=context) + # Start out with known tables with no model + 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: + continue + known_tables.append(model_pool._table) + known_tables += [ + column._sql_names(model_pool)[0] + for column in model_pool._columns.values() + if column._type == 'many2many' + # unstored function fields of type m2m don't have _rel + and hasattr(column, '_rel') + ] + + # Cannot pass table names as a psycopg argument + known_tables_repr = ",".join( + [("'%s'" % table) for table in known_tables]) + cr.execute( + """ + SELECT table_name FROM information_schema.tables + WHERE table_schema = 'public' AND table_type = 'BASE TABLE' + AND table_name NOT IN (%s)""" % known_tables_repr) + + res = [(0, 0, {'name': row[0]}) for row in cr.fetchall()] + if not res: + raise orm.except_orm( + _('Nothing to do'), + _('No orphaned tables found')) + return res + + _columns = { + 'purge_line_ids': fields.one2many( + 'cleanup.purge.line.table', + 'wizard_id', 'Tables to purge'), + } diff --git a/database_cleanup/model/purge_wizard.py b/database_cleanup/model/purge_wizard.py new file mode 100644 index 000000000..5003c4aef --- /dev/null +++ b/database_cleanup/model/purge_wizard.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# 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 . +# +############################################################################## + +import logging +from openerp.osv import orm, fields + + +class CleanupPurgeLine(orm.AbstractModel): + """ Abstract base class for the purge wizard lines """ + _name = 'cleanup.purge.line' + _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 + + _columns = { + 'name': fields.char('Name', size=64, readonly=True), + } diff --git a/database_cleanup/static/src/img/icon.png b/database_cleanup/static/src/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6980d05de2f0a9bd93df4207cc0245eb566f818a GIT binary patch literal 30647 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 G3WqHI+Gl@?7q9)=zQdDv^3R>v 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 aO#eUJ;J(3(mu?6E0000 + + + + + Database cleanup + + + + + + + Purge obsolete modules + + + + + + + Purge obsolete models + + + + + + + Purge obsolete columns + + + + + + + Purge obsolete tables + + + + + + + Purge obsolete data entries + + + + + + + diff --git a/database_cleanup/view/purge_columns.xml b/database_cleanup/view/purge_columns.xml new file mode 100644 index 000000000..40ed4a4f6 --- /dev/null +++ b/database_cleanup/view/purge_columns.xml @@ -0,0 +1,37 @@ + + + + + + Form view for purge columns wizard + cleanup.purge.wizard.column + +
+

+ +

+