Files
tryton/trytond/ir/model.py
T

457 lines
18 KiB
Python

"model"
from trytond.osv import fields, OSV
from trytond.netsvc import Logger, LocalService, LOG_ERROR, LOG_INFO, LOG_WARNING, LOG_DEBUG
from trytond.osv.orm import ExceptORM
from trytond.tools import Cache
import time
# Custom exception:
class Unhandled_field(Exception):
"""
Raised when a field type is not supported by the update mechanism.
"""
pass
class Model(OSV):
"Model"
_name = 'ir.model'
_description = __doc__
_columns = {
'name': fields.Char('Model name', size=64, translate=True),
'model': fields.Char('Object name', size=64, required=True),
'info': fields.Text('Information'),
'fields': fields.One2Many('ir.model.field', 'model', 'Fields',
required=True),
}
_defaults = {
'name': lambda *a: 'No Name',
}
Model()
class ModelField(OSV):
"Model field"
_name = 'ir.model.field'
_description = __doc__
_columns = {
'name': fields.Char('Name', size=64),
'relation': fields.Char('Model Relation', size=64),
'model': fields.Many2One('ir.model', 'Model', required=True,
select=True, ondelete='cascade'),
'field_description': fields.Char('Field Description', size=256,
translate=True),
'ttype': fields.Char('Field Type', size=64),
'relate': fields.Boolean('Click and Relate'),
'groups': fields.Many2Many('res.group', 'ir_model_field_group_rel',
'field_id', 'group_id', 'Groups'),
'group_name': fields.Char('Group Name', size=128),
'view_load': fields.Boolean('View Auto-Load'),
'help': fields.Text('Help', translate=True),
}
_defaults = {
'relate': lambda *a: 0,
'view_load': lambda *a: 0,
'name': lambda *a: 'No Name',
'field_description': lambda *a: 'No description available',
}
_order = "id"
ModelField()
class ModelAccess(OSV):
"Model access"
_name = 'ir.model.access'
_description = __doc__
_rec_name = 'model'
_columns = {
'model': fields.Many2One('ir.model', 'Model', required=True),
'group': fields.Many2One('res.group', 'Group'),
'perm_read': fields.Boolean('Read Access'),
'perm_write': fields.Boolean('Write Access'),
'perm_create': fields.Boolean('Create Access'),
'perm_unlink': fields.Boolean('Delete Permission'),
'description': fields.Text('Description'),
}
_sql_constraints = [
('model_group_uniq', 'UNIQUE("model", "group")',
'Only on record by model and group is allowed!'),
]
def check(self, cursor, user, model_name, mode='read',
raise_exception=True):
assert mode in ['read', 'write', 'create', 'unlink'], \
'Invalid access mode for security'
if user == 0:
return True
cursor.execute('SELECT MAX(CASE WHEN perm_'+mode+' THEN 1 else 0 END) '
'FROM ir_model_access a '
'JOIN ir_model m '
'ON (a.model = m.id) '
'JOIN res_group_user_rel gu '
'ON (gu.gid = a.group) '
'WHERE m.model = %s AND gu.uid = %s', (model_name, user,))
row = cursor.fetchall()
if row[0][0] == None:
cursor.execute('SELECT ' \
'MAX(CASE WHEN perm_' + mode + ' THEN 1 else 0 END) ' \
'FROM ir_model_access a ' \
'JOIN ir_model m ' \
'ON (a.model = m.id) ' \
'WHERE a.group IS NULL AND m.model = %s', (model_name,))
row = cursor.fetchall()
if row[0][0] == None:
return True
if not row[0][0]:
if raise_exception:
if mode == 'read':
raise ExceptORM('AccessError',
'You can not read this document! (%s)' \
% model_name)
elif mode == 'write':
raise ExceptORM('AccessError',
'You can not write in this document! (%s)' \
% model_name)
elif mode == 'create':
raise ExceptORM('AccessError',
'You can not create this kind of document! (%s)' \
% model_name)
elif mode == 'unlink':
raise ExceptORM('AccessError',
'You can not delete this document! (%s)' \
% model_name)
raise ExceptORM('AccessError',
'You do not have access to this document! (%s)' \
% model_name)
else:
return False
return True
check = Cache()(check)
# Methods to clean the cache on the Check Method.
def write(self, cursor, user, ids, vals, context=None):
res = super(ModelAccess, self).write(cursor, user, ids, vals,
context=context)
self.check()
return res
def create(self, cursor, user, vals, context=None):
res = super(ModelAccess, self).create(cursor, user, vals,
context=context)
self.check()
return res
def unlink(self, cursor, user, ids, context=None):
res = super(ModelAccess, self).unlink(cursor, user, ids,
context=context)
self.check()
return res
ModelAccess()
class ModelData(OSV):
"Model data"
_name = 'ir.model.data'
_description = __doc__
_columns = {
'fs_id': fields.Char('Identifier on File System', required=True,
size=64, help="The id of the record as known on the file system."),
'model': fields.Char('Model', required=True, size=64),
'module': fields.Char('Module', required=True, size=64),
'db_id': fields.Integer('Resource ID',
help="The id of the record in the database."),
'date_update': fields.DateTime('Update Date'),
'date_init': fields.DateTime('Init Date'),
'values': fields.Text('Values'),
}
_defaults = {
'date_init': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
}
_sql_constraints = [
('fs_id_module_uniq', 'UNIQUE("fs_id", "module")',
'The couple (fs_id, module) must be unique!'),
]
def __init__(self, pool):
super(ModelData, self).__init__(pool)
if pool:
self.fs2db = None
self.fs2values = None
def populate_fs2db(self, cursor, user):
"""Fetch all the db_id, model tuple in the table in one shot
(if necessary). This table is kept as a cache for the future
conversions between fs_id abd db_id"""
if not self.fs2db: self.fs2db = {}
if cursor.dbname not in self.fs2db:
module_data_ids = self.search(cursor, user,[])
self.fs2db[cursor.dbname] = dict([
((x.fs_id, x.module),
(x.db_id, x.model, x.id)) for x in \
self.browse(cursor, user, module_data_ids)])
def populate_fs2values(self, cursor, user):
"""Fetch all the table in one shot (if necessary). When a
record is imported ny import_record, the corresponding item in
fs2values is removed. When post_import is called, the items
remaining in fs2values are the one that were in the model_data
table but not in the fs, so they are removed."""
# Improvement: store the fiedls names and a hash of the values
# instead of all the values.
if not self.fs2values: self.fs2values = {}
if cursor.dbname not in self.fs2values:
module_data_ids = self.search(cursor, user,[])
self.fs2values[cursor.dbname] = dict([
((x.fs_id, x.module),
x.values) for x in \
self.browse(cursor, user, module_data_ids)])
def get_id(self, cursor, user, module, fs_id):
# Pre-Fetch all the data at once
self.populate_fs2db(cursor, user)
# return only the db_id
if (fs_id, module) in self.fs2db[cursor.dbname]:
return self.fs2db[cursor.dbname][(fs_id, module)][0]
else:
raise Exception("Reference to %s not found"%\
".".join([module,fs_id]))
@staticmethod
def _clean_value(key, browse_record, object_ref):
"""
Take a field name, a browse_record, and a reference to the
corresponding object. Return a raw value has it must look on the
db.
"""
# search the field type in the object or in a parent
if key in object_ref._columns:
field_type = object_ref._columns[key]._type
else:
field_type = object_ref._inherit_fields[key][2]._type
# handle the value regarding to the type
if field_type == 'many2one':
return browse_record[key] and browse_record[key].id or False
elif field_type in ['one2one', 'one2many', "many2many"]:
raise Unhandled_field()
else:
return browse_record[key]
def import_record(self, cursor, user, model, module, values, fs_id):
if not fs_id:
raise Exception('import_record : Argument fs_id is mandatory')
if '.' in fs_id:
assert len(fs_id.split('.')) == 2, '"%s" contains too many dots. '\
'file system ids should contain ot most one dot ! ' \
'These are used to refer to other modules data, ' \
'as in module.reference_id' % (fs_id)
module, fs_id = fs_id.split('.')
self.populate_fs2db(cursor, user)
self.populate_fs2values(cursor, user)
object_ref = self.pool.get(model)
if (fs_id, module) in self.fs2db[cursor.dbname]:
# this record is already in the db:
db_id, db_model, mdata_id = \
self.fs2db[cursor.dbname][(fs_id, module)]
old_values = self.fs2values[cursor.dbname][(fs_id, module)]
if old_values == None:
old_values = {}
else:
old_values = eval(old_values)
# Check if values for this record has been modified in the
# db, if not it's ok to overwrite them.
if model != db_model:
raise Exception("This record try to overwrite"
"data with the wrong model: %s (module: %s)"% (fs_id, module))
#Re-create object if it was deleted
if not object_ref.search(cursor, user, [('id', '=', db_id)]):
db_id = object_ref.create(cursor, user, values,
context={'module': module})
data_id = self.search(cursor, user, [
('fs_id', '=', fs_id),
('module', '=', module),
], limit=1)[0]
self.write(cursor, user, data_id, {
'db_id': db_id,
})
self.fs2db[cursor.dbname][(fs_id, module)] = \
(db_id, db_model, mdata_id)
db_values = object_ref.browse(cursor, user, db_id)
to_update = {}
for key in values:
try:
db_field = ModelData._clean_value(key, db_values,
object_ref)
except Unhandled_field:
logger = Logger()
logger.notify_channel('init', LOG_WARNING,
'Field %s on %s : integrity not tested.'%(key, model))
to_update[key] = values[key]
continue
# if the fs value is the same has in the db, whe ignore it
if db_field == values[key]:
continue
# we cannot update a field if it was changed by a user
if key not in old_values:
default_value = object_ref._defaults.get(
key, lambda *a:None)(cursor, user)
if db_field != default_value:
logger = Logger()
logger.notify_channel('init', LOG_WARNING,
"Field %s of %s@%s not updated (id: %s), because "\
"it has changed since the last update"% \
(key, db_id, model, fs_id))
continue
elif (old_values[key] and db_field) and \
old_values[key] != db_field:
logger = Logger()
logger.notify_channel('init', LOG_WARNING,
"Field %s of %s@%s not updated (id: %s), because "\
"it has changed since the last update."%\
(key, db_id, model, fs_id))
continue
# so, the field in the fs and in the db are different,
# and no user changed the value in the db:
to_update[key] = values[key]
# if there is values to update:
if to_update:
# write the values in the db:
object_ref.write(cursor, user, db_id, to_update)
# re-read it: this ensure that we store the real value
# in the model_data table:
db_val = object_ref.browse(cursor, user, db_id)
for key in to_update:
try:
values[key] = ModelData._clean_value(
key, db_val, object_ref)
except Unhandled_field:
continue
if values != old_values:
self.write(cursor, user, mdata_id, {
'fs_id': fs_id,
'model': model,
'module': module,
'db_id': db_id,
'values': str(values),
'date_update': time.strftime('%Y-%m-%d %H:%M:%S'),
})
# Remove this record from the value list. This means that
# the corresponding record have been found.
self.fs2values[cursor.dbname][(fs_id, module)] = None
else:
# this record is new, create it in the db:
db_id = object_ref.create(cursor, user, values,
context={'module': module})
# re-read it: this ensure that we store the real value
# in the model_data table:
db_val = object_ref.browse(cursor, user, db_id)
for key in values:
try:
values[key] = ModelData._clean_value(key, db_val,
object_ref)
except Unhandled_field:
continue
mdata_id = self.create(cursor, user, {
'fs_id': fs_id,
'model': model,
'module': module,
'db_id': db_id,
'values': str(values),
})
# update fs2db:
self.fs2db[cursor.dbname][(fs_id, module)]= (db_id, model,
mdata_id)
self.fs2values[cursor.dbname][(fs_id, module)] = None
def post_import(self, cursor, user, modules):
# Test because of a wrong extra call. See todo at the end of
# load_module_graph in module.py
# Globaly this function is a bit dirty.
if not (self.fs2values and self.fs2values.get(cursor.dbname)):
return True
wf_service = LocalService("workflow")
mdata_unlink = []
for (fs_id, module) in self.fs2values[cursor.dbname]:
if self.fs2values[cursor.dbname][(fs_id, module)] == None:
continue
if module in modules:
(db_id, model, mdata_id) = self.fs2db[cursor.dbname]\
[(fs_id, module)]
if model == 'workflow.activity':
cursor.execute('SELECT res_type, db_id ' \
'FROM wkf_instance ' \
'WHERE id IN (' \
'SELECT instance FROM wkf_workitem ' \
'WHERE act_id = %d)', (db_id,))
wkf_todo.extend(cursor.fetchall())
cursor.execute("UPDATE wkf_transition " \
"SET condition = 'True', role_id = NULL, " \
"signal = NULL, act_to = act_from, " \
"act_from = %d " \
"WHERE act_to = %d", (db_id, db_id))
cursor.execute("DELETE FROM wkf_transition " \
"WHERE act_to = %d", (db_id,))
wf_service.trg_write(user, model, db_id, cursor)
logger = Logger()
logger.notify_channel('init', LOG_INFO,
'Deleting %s@%s' % (db_id, model))
try:
self.pool.get(model).unlink(cursor, user, db_id)
mdata_unlink.append(mdata_id)
except:
raise
logger.notify_channel('init', LOG_ERROR,
'Could not delete id: %d of model %s\n' \
'There should be some relation ' \
'that points to this resource\n' \
'You should manually fix this ' \
'and restart --update=module' % \
(db_id, model))
if mdata_unlink:
self.unlink(cursor, user, mdata_unlink)
del self.fs2values[cursor.dbname]
return True
ModelData()