# -*- coding: UTF-8 -*- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. from trac.db import Table, Column from trac.core import TracError from trac.resource import ResourceNotFound, ResourceSystem from trac.ticket.api import TicketSystem def dict_to_kv_str(env, data=None, sep=' AND '): """Converts a dictionary into a string and a list suitable for using as part of an SQL where clause like: ('key0=%s AND key1=%s', ['value0','value1']) The sep argument allows ' AND ' to be changed for ',' for UPDATE purposes """ if data is None: return ('', []) qfn = env.get_read_db().quote return (sep.join('%s=%%s' % qfn(k) for k in data.keys()), data.values()) def fields_to_kv_str(env, fields, data, sep=' AND '): """Converts a list of fields and a dictionary containing those fields into a string and a list suitable for using as part of an SQL where clause like: ('key0=%s,key1=%s', ['value0','value1']) """ return dict_to_kv_str(env, dict((f, data[f]) for f in fields),sep) class ModelBase(object): """Base class for the models to factor out common features Derived classes should provide a meta dictionary to describe the table like: _meta = {'table_name':'mytable', 'object_name':'WhatIWillCallMyselfInMessages', 'key_fields':['id','id2'], 'non_key_fields':[ 'thing', { name:"field_name_x", type='int64', size=None, key_size=None, auto_increment=False }], 'auto_inc_fields': ['id',], } key_fields and non_key_fields parameters may contain field name only (for text columns) or dict with detailed column specification. In case of detailed column specification 'name' parameter is obligatory). """ def __init__(self, env, keys=None): """Initialisation requires an environment to be specified. If keys are provided, the Model will initialise from the database """ # make this impossible to instantiate without telling the class details # about itself in the self.meta dictionary self._old_data = {} self._data = {} self._exists = False self._env = env self._key_fields = self._get_field_names(self._meta['key_fields']) self._non_key_fields = self._get_field_names( self._meta['non_key_fields']) self._all_fields = self._key_fields + self._non_key_fields self._unique_fields = self._meta['unique_fields'] self._auto_inc_fields = self._get_auto_inc_field_names() if keys is not None: self._get_row(keys) else: self._update_from_row(None) def update_field_dict(self, field_dict): """Updates the object's copy of the db fields (no db transaction)""" self._data.update(field_dict) def __getattr__(self, name): """Overridden to allow table.field style field access.""" try: if name in self._all_fields: return self._data[name] except KeyError: raise AttributeError(name) raise AttributeError(name) def __setattr__(self, name, value): """Overridden to allow table.field = value style field setting.""" data = self.__dict__.get('_data') fields = self.__dict__.get('_all_fields') if data and fields and name in fields: self._data[name] = value else: dict.__setattr__(self, name, value) @classmethod def get_table_name(cls): return cls._meta["table_name"] def _update_from_row(self, row = None): """uses a provided database row to update the model""" fields = self._all_fields self._exists = row is not None if row is None: row = [None]*len(fields) self._data = dict([(fields[i], row[i]) for i in range(len(row))]) self._old_data = {} self._old_data.update(self._data) def _get_row(self, keys): """queries the database and stores the result in the model""" row = None where, values = fields_to_kv_str(self._env, self._key_fields, keys) fields = ','.join(self._all_fields) sdata = {'fields':fields, 'where':where} sdata.update(self._meta) sql = """SELECT %(fields)s FROM %(table_name)s WHERE %(where)s""" % sdata with self._env.db_query as db: for row in db(sql, values): self._update_from_row(row) break else: raise ResourceNotFound( ('No %(object_name)s with %(where)s' % sdata) % tuple(values)) def delete(self): """Deletes the matching record from the database""" if not self._exists: raise TracError('%(object_name)s does not exist' % self._meta) where, values = fields_to_kv_str(self._env, self._key_fields, self._data) sdata = {'where': where} sdata.update(self._meta) sql = """DELETE FROM %(table_name)s WHERE %(where)s""" % sdata with self._env.db_transaction as db: db(sql, values) self._exists = False TicketSystem(self._env).reset_ticket_fields() ResourceSystem(self._env).resource_deleted(self) self._data = dict([(k, None) for k in self._data.keys()]) self._old_data.update(self._data) def insert(self): """Create new record in the database""" sdata = None if self._exists or len(self.select(self._env, where = dict([(k,self._data[k]) for k in self._key_fields]))): sdata = {'keys':','.join(["%s='%s'" % (k, self._data[k]) for k in self._key_fields])} elif self._unique_fields and len(self.select(self._env, where = dict([(k,self._data[k]) for k in self._unique_fields]))): sdata = {'keys':','.join(["%s='%s'" % (k, self._data[k]) for k in self._unique_fields])} if sdata: sdata.update(self._meta) sdata['values'] = self._data raise TracError('%(object_name)s %(keys)s already exists %(values)s' % sdata) for key in self._key_fields: if self._data[key] is None and key not in self._auto_inc_fields: sdata = {'key':key} sdata.update(self._meta) raise TracError('%(key)s required for %(object_name)s' % sdata) fields = [field for field in self._all_fields if field not in self._auto_inc_fields] sdata = {'fields':','.join(fields), 'values':','.join(['%s'] * len(fields))} sdata.update(self._meta) sql = """INSERT INTO %(table_name)s (%(fields)s) VALUES (%(values)s)""" % sdata with self._env.db_transaction as db: cursor = db.cursor() cursor.execute(sql, [self._data[f] for f in fields]) for auto_in_field in self._auto_inc_fields: self._data[auto_in_field] = db.get_last_id( cursor, sdata["table_name"], auto_in_field) self._exists = True self._old_data.update(self._data) TicketSystem(self._env).reset_ticket_fields() ResourceSystem(self._env).resource_created(self) def _update_relations(self, db, author=None): """Extra actions due to update""" pass def update(self, author=None): """Update the matching record in the database""" if self._old_data == self._data: return if not self._exists: raise TracError('%(object_name)s does not exist' % self._meta) for key in self._meta['no_change_fields']: if self._data[key] != self._old_data[key]: raise TracError('%s cannot be changed' % key) for key in self._key_fields + self._unique_fields: if self._data[key] != self._old_data[key]: if len(self.select(self._env, where = {key:self._data[key]})): raise TracError('%s already exists' % key) setsql, setvalues = fields_to_kv_str(self._env, self._non_key_fields, self._data, sep=',') where, values = fields_to_kv_str(self._env, self._key_fields, self._data) sdata = {'where': where, 'values': setsql} sdata.update(self._meta) sql = """UPDATE %(table_name)s SET %(values)s WHERE %(where)s""" % sdata old_values = dict((k, v) for k, v in self._old_data.iteritems() if self._data.get(k) != v) with self._env.db_transaction as db: db(sql, setvalues + values) self._update_relations(db, author) self._old_data.update(self._data) TicketSystem(self._env).reset_ticket_fields() ResourceSystem(self._env).resource_changed(self, old_values) @classmethod def select(cls, env, db=None, where=None, limit=None, order_by=None): """ Query the database to get a set of records back * order_by: is list of fields with optional sort direction ("asc" or "desc") e.g. ["field1", "field2 desc"] """ rows = [] fields = cls._get_all_field_names() sdata = {'fields': ','.join(env.get_read_db().quote(f) for f in fields),} sdata.update(cls._meta) sql = r'SELECT %(fields)s FROM %(table_name)s' % sdata wherestr, values = dict_to_kv_str(env, where) if wherestr: wherestr = ' WHERE ' + wherestr final_sql = sql + wherestr if limit is not None: final_sql += ' LIMIT ' + str(int(limit)) if order_by: final_sql += "\nORDER BY " + ', '.join(order_by) for row in env.db_query(final_sql, values): # we won't know which class we need until called model = cls.__new__(cls) data = dict([(fields[i], row[i]) for i in range(len(fields))]) model.__init__(env, data) rows.append(model) return rows @classmethod def _get_all_field_names(cls): return cls._get_field_names( cls._meta['key_fields']+cls._meta['non_key_fields']) @classmethod def _get_field_names(cls, field_specs): def get_field_name(field_spec): if isinstance(field_spec, dict): return field_spec["name"] return field_spec return [get_field_name(field_spec) for field_spec in field_specs] @classmethod def _get_all_field_columns(cls): auto_inc = cls._meta.get('auto_inc_fields', []) columns = [] all_fields_spec = cls._meta['key_fields'] + cls._meta['non_key_fields'] for field_spec in all_fields_spec: #field_spec can be field name string or dictionary with detailed #column specification if isinstance(field_spec, dict): column_spec = field_spec else: column_spec = dict( name = field_spec, auto_increment=field_spec in auto_inc) columns.append(column_spec) return columns @classmethod def _get_auto_inc_field_names(cls): return [field_spec["name"] for field_spec in cls._get_all_field_columns() if field_spec.get("auto_increment")] @classmethod def _get_schema(cls): """Generate schema from the class meta data""" fields = [Column( column_spec["name"], type=column_spec.get("type", "text"), size=column_spec.get("size"), key_size=column_spec.get("key_size"), auto_increment=column_spec.get("auto_increment", False)) for column_spec in cls._get_all_field_columns()] return Table(cls._meta['table_name'], key=set(cls._meta['key_fields'] + cls._meta['unique_fields'])) [fields]