~openupgrade-committers/openupgrade-server/5.0

« back to all changes in this revision

Viewing changes to bin/openupgrade/openupgrade.py

  • Committer: Stefan Rijnhart
  • Date: 2012-05-27 12:24:43 UTC
  • mfrom: (2175.1.3 openupgrade-server)
  • Revision ID: stefan@therp.nl-20120527122443-7lh1cvrm26wt8u6e
[MRG] Add openupgrade development module for easy analysis file generation

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
import os
 
3
from osv import osv
 
4
import pooler
 
5
import logging
 
6
import tools
 
7
import openupgrade_tools
 
8
 
 
9
logger = logging.getLogger('OpenUpgrade')
 
10
 
 
11
__all__ = [
 
12
    'load_data',
 
13
    'rename_columns',
 
14
    'rename_tables',
 
15
    'drop_columns',
 
16
    'table_exists',
 
17
    'column_exists',
 
18
    'delete_model_workflow',
 
19
    'set_defaults',
 
20
    'update_module_names',
 
21
    'add_ir_model_fields',
 
22
]    
 
23
 
 
24
def load_data(cr, module_name, filename, idref=None, mode='init'):
 
25
    """
 
26
    Load an xml or csv data file from your post script. The usual case for this is the
 
27
    occurrence of newly added essential or useful data in the module that is
 
28
    marked with "noupdate='1'" and without "forcecreate='1'" so that it will
 
29
    not be loaded by the usual upgrade mechanism. Leaving the 'mode' argument to
 
30
    its default 'init' will load the data from your migration script.
 
31
    
 
32
    Theoretically, you could simply load a stock file from the module, but be 
 
33
    careful not to reinitialize any data that could have been customized.
 
34
    Preferably, select only the newly added items. Copy these to a file
 
35
    in your migrations directory and load that file.
 
36
    Leave it to the user to actually delete existing resources that are
 
37
    marked with 'noupdate' (other named items will be deleted
 
38
    automatically).
 
39
 
 
40
 
 
41
    :param module_name: the name of the module
 
42
    :param filename: the path to the filename, relative to the module \
 
43
    directory.
 
44
    :param idref: optional hash with ?id mapping cache?
 
45
    :param mode: one of 'init', 'update', 'demo'. Always use 'init' for adding new items \
 
46
    from files that are marked with 'noupdate'. Defaults to 'init'.
 
47
 
 
48
    """
 
49
 
 
50
    if idref is None:
 
51
        idref = {}
 
52
    logger.info('%s: loading %s' % (module_name, filename))
 
53
    _, ext = os.path.splitext(filename)
 
54
    pathname = os.path.join(module_name, filename)
 
55
    fp = tools.file_open(pathname)
 
56
    try:
 
57
        if ext == '.csv':
 
58
            noupdate = True
 
59
            tools.convert_csv_import(cr, module_name, pathname, fp.read(), idref, mode, noupdate)
 
60
        else:
 
61
            tools.convert_xml_import(cr, module_name, fp, idref, mode=mode)
 
62
    finally:
 
63
        fp.close()
 
64
 
 
65
# for backwards compatibility
 
66
load_xml = load_data
 
67
table_exists = openupgrade_tools.table_exists
 
68
 
 
69
def rename_columns(cr, column_spec):
 
70
    """
 
71
    Rename table columns. Typically called in the pre script.
 
72
 
 
73
    :param column_spec: a hash with table keys, with lists of tuples as values. \
 
74
    Tuples consist of (old_name, new_name).
 
75
 
 
76
    """
 
77
    for table in column_spec.keys():
 
78
        for (old, new) in column_spec[table]:
 
79
            logger.info("table %s, column %s: renaming to %s",
 
80
                     table, old, new)
 
81
            cr.execute('ALTER TABLE "%s" RENAME "%s" TO "%s"' % (table, old, new,))
 
82
 
 
83
def rename_tables(cr, table_spec):
 
84
    """
 
85
    Rename tables. Typically called in the pre script.
 
86
    :param column_spec: a list of tuples (old table name, new table name).
 
87
 
 
88
    """
 
89
    for (old, new) in table_spec:
 
90
        logger.info("table %s: renaming to %s",
 
91
                    old, new)
 
92
        cr.execute('ALTER TABLE "%s" RENAME TO "%s"' % (old, new,))
 
93
 
 
94
def rename_models(cr, model_spec):
 
95
    """
 
96
    Rename models. Typically called in the pre script.
 
97
    :param column_spec: a list of tuples (old table name, new table name).
 
98
    
 
99
    Use case: if a model changes name, but still implements equivalent
 
100
    functionality you will want to update references in for instance
 
101
    relation fields.
 
102
 
 
103
    """
 
104
    for (old, new) in model_spec:
 
105
        logger.info("model %s: renaming to %s",
 
106
                    old, new)
 
107
        cr.execute('UPDATE ir_model_fields SET relation = %s '
 
108
                   'WHERE relation = %s', (new, old,))
 
109
 
 
110
def drop_columns(cr, column_spec):
 
111
    """
 
112
    Drop columns but perform an additional check if a column exists.
 
113
    This covers the case of function fields that may or may not be stored.
 
114
    Consider that this may not be obvious: an additional module can govern
 
115
    a function fields' store properties.
 
116
 
 
117
    :param column_spec: a list of (table, column) tuples
 
118
    """
 
119
    for (table, column) in column_spec:
 
120
        logger.info("table %s: drop column %s",
 
121
                    table, column)
 
122
        if column_exists(cr, table, column):
 
123
            cr.execute('ALTER TABLE "%s" DROP COLUMN "%s"' % 
 
124
                       (table, column))
 
125
        else:
 
126
            logger.warn("table %s: column %s did not exist",
 
127
                    table, column)
 
128
 
 
129
def delete_model_workflow(cr, model):
 
130
    """ 
 
131
    Forcefully remove active workflows for obsolete models,
 
132
    to prevent foreign key issues when the orm deletes the model.
 
133
    """
 
134
    logged_query(
 
135
        cr,
 
136
        "DELETE FROM wkf_workitem WHERE act_id in "
 
137
        "( SELECT wkf_activity.id "
 
138
        "  FROM wkf_activity, wkf "
 
139
        "  WHERE wkf_id = wkf.id AND "
 
140
        "  wkf.osv = %s"
 
141
        ")", (model,))
 
142
    logged_query(
 
143
        cr,
 
144
        "DELETE FROM wkf WHERE osv = %s", (model,))
 
145
 
 
146
def set_defaults(cr, pool, default_spec, force=False):
 
147
    """
 
148
    Set default value. Useful for fields that are newly required. Uses orm, so
 
149
    call from the post script.
 
150
    
 
151
    :param default_spec: a hash with model names as keys. Values are lists of \
 
152
    tuples (field, value). None as a value has a special meaning: it assigns \
 
153
    the default value. If this value is provided by a function, the function is \
 
154
    called as the user that created the resource.
 
155
    :param force: overwrite existing values. To be used for assigning a non- \
 
156
    default value (presumably in the case of a new column). The ORM assigns \
 
157
    the default value as declared in the model in an earlier stage of the \
 
158
    process. Beware of issues with resources loaded from new data that \
 
159
    actually do require the model's default, in combination with the post \
 
160
    script possible being run multiple times.
 
161
    """
 
162
 
 
163
    def write_value(ids, field, value):
 
164
        logger.info("model %s, field %s: setting default value of %d resources to %s",
 
165
                 model, field, len(ids), unicode(value))
 
166
        obj.write(cr, 1, ids, {field: value})
 
167
 
 
168
    for model in default_spec.keys():
 
169
        obj = pool.get(model)
 
170
        if not obj:
 
171
            raise osv.except_osv("Migration: error setting default, no such model: %s" % model, "")
 
172
 
 
173
    for field, value in default_spec[model]:
 
174
        domain = not force and [(field, '=', False)] or []
 
175
        ids = obj.search(cr, 1, domain)
 
176
        if not ids:
 
177
            continue
 
178
        if value is None:
 
179
            # Set the value by calling the _defaults of the object.
 
180
            # Typically used for company_id on various models, and in that
 
181
            # case the result depends on the user associated with the object.
 
182
            # We retrieve create_uid for this purpose and need to call the _defaults
 
183
            # function per resource. Otherwise, write all resources at once.
 
184
            if field in obj._defaults:
 
185
                if not callable(obj._defaults[field]):
 
186
                    write_value(ids, field, obj._defaults[field])
 
187
                else:
 
188
                    # existence users is covered by foreign keys, so this is not needed
 
189
                    # cr.execute("SELECT %s.id, res_users.id FROM %s LEFT OUTER JOIN res_users ON (%s.create_uid = res_users.id) WHERE %s.id IN %s" %
 
190
                    #                     (obj._table, obj._table, obj._table, obj._table, tuple(ids),))
 
191
                    cr.execute("SELECT id, COALESCE(create_uid, 1) FROM %s " % obj._table + "WHERE id in %s", (tuple(ids),))
 
192
                    fetchdict = dict(cr.fetchall())
 
193
                    for id in ids:
 
194
                        write_value([id], field, obj._defaults[field](obj, cr, fetchdict.get(id, 1), None))
 
195
                        if id not in fetchdict:
 
196
                            logger.info("model %s, field %s, id %d: no create_uid defined or user does not exist anymore",
 
197
                                     model, field, id)
 
198
            else:
 
199
                error = ("OpenUpgrade: error setting default, field %s with "
 
200
                         "None default value not in %s' _defaults" % (
 
201
                        field, model))
 
202
                logger.error(error)
 
203
                # this exeption seems to get lost in a higher up try block
 
204
                osv.except_osv("OpenUpgrade", error)
 
205
        else:
 
206
            write_value(ids, field, value)
 
207
    
 
208
def logged_query(cr, query, args=None):
 
209
    if args is None:
 
210
        args = []
 
211
    res = cr.execute(query, args)
 
212
    logger.debug('Running %s', query)
 
213
    if not res:
 
214
        query = query % args
 
215
        logger.warn('No rows affected for query "%s"', query)
 
216
    return res
 
217
 
 
218
def column_exists(cr, table, column):
 
219
    """ Check whether a certain column exists """
 
220
    cr.execute(
 
221
        'SELECT count(attname) FROM pg_attribute '
 
222
        'WHERE attrelid = '
 
223
        '( SELECT oid FROM pg_class WHERE relname = %s ) '
 
224
        'AND attname = %s',
 
225
        (table, column));
 
226
    return cr.fetchone()[0] == 1
 
227
 
 
228
def update_module_names(cr, namespec):
 
229
    """
 
230
    Deal with changed module names of certified modules
 
231
    in order to prevent  'certificate not unique' error,
 
232
    as well as updating the module reference in the
 
233
    XML id.
 
234
    
 
235
    :param namespec: tuple of (old name, new name)
 
236
    """
 
237
    for (old_name, new_name) in namespec:
 
238
        query = ("UPDATE ir_module_module SET name = %s "
 
239
                 "WHERE name = %s")
 
240
        logged_query(cr, query, (new_name, old_name))
 
241
        query = ("UPDATE ir_model_data SET module = %s "
 
242
                 "WHERE module = %s ")
 
243
        logged_query(cr, query, (new_name, old_name))
 
244
 
 
245
def add_ir_model_fields(cr, columnspec):
 
246
    """
 
247
    Typically, new columns on ir_model_fields need to be added in a very
 
248
    early stage in the upgrade process of the base module, in raw sql
 
249
    as they need to be in place before any model gets initialized.
 
250
    Do not use for fields with additional SQL constraints, such as a
 
251
    reference to another table or the cascade constraint, but craft your
 
252
    own statement taking them into account.
 
253
    
 
254
    :param columnspec: tuple of (column name, column type)
 
255
    """
 
256
    for column in columnspec:
 
257
        query = 'ALTER TABLE ir_model_fields ADD COLUMN %s %s' % (
 
258
            column)
 
259
        logged_query(cr, query, [])