source: peerreviewplugin/tags/0.12/3.1/codereview/tracgenericclass/model.py

Last change on this file was 16451, checked in by Ryan J Ollos, 6 years ago

Fix indentation

File size: 52.2 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2010-2015 Roberto Longobardi
4#
5# This file is part of the Test Manager plugin for Trac.
6#
7# This software is licensed as described in the file COPYING, which
8# you should have received as part of this distribution. The terms
9# are also available at:
10#   https://trac-hacks.org/wiki/TestManagerForTracPluginLicense
11#
12# Author: Roberto Longobardi <otrebor.dev@gmail.com>
13#
14
15import copy
16from datetime import date, datetime
17import re
18
19from trac.core import Interface, TracError, Component, ExtensionPoint
20from trac.db import Table, Column, Index, DatabaseManager, with_transaction
21from trac.resource import Resource
22from trac.util.datefmt import utc
23from trac.util.translation import _
24from trac.wiki.model import WikiPage
25from trac.wiki.web_ui import WikiModule
26
27from codereview.tracgenericclass.util import from_any_timestamp, get_string_from_dictionary, \
28    to_any_timestamp, to_list, get_timestamp_db_type, list_available_tables, \
29    db_get_config_property
30
31
32class IConcreteClassProvider(Interface):
33    """
34    Extension point interface for components willing to implement
35    concrete classes based on this generic class framework.
36    """
37
38    def get_realms(self):
39        """
40        Return class realms provided by the component.
41
42        :rtype: `basestring` generator
43        """
44
45    def get_data_models(self):
46        """
47        Return database tables metadata to allow the framework to create the
48        db schema for the classes provided by the component.
49
50        :rtype: a dictionary, which keys are schema names and values
51                are dictionaries with table metadata, as in the following example:
52                return {'sample_realm':
53                            {'table':
54                                Table('samplerealm', key = ('id', 'otherid'))[
55                                      Column('id'),
56                                      Column('otherid'),
57                                      Column('prop1'),
58                                      Column('prop2'),
59                                      Column('time', type='int64')],
60                             'has_custom': True,
61                             'has_change': True},
62                       }
63        """
64
65    def get_fields(self):
66        """
67        Return the standard fields for classes in all the realms
68        provided by the component.
69
70        :rtype: a dictionary, which keys are realm names and values
71                are arrays of fields metadata, as in the following example:
72                return {'sample_realm': [
73                            {'name': 'id', 'type': 'text', 'label': N_('ID')},
74                            {'name': 'otherid', 'type': 'text', 'label': N_('Other ID')},
75                            {'name': 'prop1', 'type': 'text', 'label': N_('Property 1')},
76                            {'name': 'prop2', 'type': 'text', 'label': N_('Property 2')},
77                            {'name': 'time', 'type': 'time', 'label': N_('Last Change')}
78                       }
79        """
80
81    def get_metadata(self):
82        """
83        Return a set of metadata about the classes in all the realms
84        provided by the component.
85
86        :rtype: a dictionary, which keys are realm names and values
87                are dictionaries of properties.
88
89                Available metadata properties are:
90                    label: A User-friendly name for the objects in this class.
91                    searchable: If present and equal to True indicates the class
92                                partecipates in the Trac search framework, and
93                                must implement the get_search_results() method.
94                    'has_custom': If present and equal to True indicates the class
95                                  supports custom fields.
96                    'has_change': If present and equal to True indicates the class
97                                  supports property change history.
98
99                See the following example:
100                return {'sample_realm': {
101                                'label': "Sample Realm",
102                                'searchable': True
103                            }
104                       }
105        """
106
107    def create_instance(self, realm, props=None):
108        """
109        Return an instance of the specified realm, with the specified properties,
110        or an empty object if props is None.
111
112        :rtype: `AbstractVariableFieldsObject` sub-class instance
113        """
114
115
116    def check_permission(self, req, realm, key_str=None, operation='set', name=None, value=None):
117        """
118        Checks whether the logged in User has permission to perform
119        the specified operation on a resource of the specified realm and
120        optionally with the specified key.
121
122        Raise an exception if authorization is denied.
123
124        Possible operations are:
125            'set': set a property with a value. 'name' and 'value' parameters are required.
126            'search': search for objects of this class.
127
128        :param key_str: optional, the object's key, in the form of a string representing
129                        a dictionary. To get a dictionary back from this string, use the
130                        get_dictionary_from_string() function in the
131                        tracgenericclass.util package.
132        :param operation: optional, the operation to be performed on the object.
133        :param name: optional property name, valid for the 'set' operation type
134        :param value: optional property value, valid for the 'set' operation type
135        """
136
137
138class AbstractVariableFieldsObject(object):
139    """
140    An object which fields are declaratively specified.
141
142    The specific object "type" is specified during construction
143    as the "realm" parameter.
144    This name must also correspond to the database table storing the
145    corresponding objects, and is used as the base name for the
146    custom fields table and the change tracking table (see below).
147
148    Features:
149        * Support for custom fields, specified in the trac.ini file
150          with the same syntax as for custom Ticket fields. Custom
151          fields are kept in a "<schema>_custom" table
152        * Keeping track of all changes to any field, into a separate
153          "<schema>_change" table
154        * A set of callbacks to allow for subclasses to control and
155          perform actions pre and post any operation pertaining the
156          object's lifecycle
157        * Registering listeners, via the IGenericObjectChangeListener
158          interface, for object creation, modification and deletion.
159        * Searching objects matching any set of valorized fields,
160          (even non-key fields), applying the "dynamic record" pattern.
161          See the method list_matching_objects.
162
163    Notes on special fields:
164
165        self.exists : always tells whether the object currently exists
166                      in the database.
167
168        self.resource: points to a Resource, in the trac environment,
169                       corresponding to this object. This is used, for
170                       example, in the workflow implementation.
171
172        self.fields: points to an array of dictionary objects describing
173                     name, label, type and other properties of all of
174                     this object's fields.
175
176        self.metadata: points to a dictionary object describing
177                       further meta-data about this object.
178
179    Note: database tables for specific realms are supposed to already
180          exist, this object does not create any tables.
181          See below the GenericClassModelProvider to see how to
182          declaratively create the required tables.
183    """
184
185    def __init__(self, env, realm='variable_fields_obj', key=None, db=None):
186        """
187        Creates an empty object and also tries to fetches it from the
188        database, if an object with a matching key is found.
189
190        To create an empty, template object, do not specify a key.
191
192        To create an object to be later stored in the database:
193           1) specify a key at contruction time
194           2) set any other property via the obj['fieldname'] = value
195              syntax, including custom fields
196           3) call the insert() method.
197
198        To fetch an existing object from the database:
199           1) specify a key at contruction time: the object will be
200            filled with all of the values form the database
201           2) modify any other property via the obj['fieldname'] = value
202              syntax, including custom fields. This syntax is the only
203              one to keep track of the changes to any field
204           3) call the save_changes() method.
205        """
206        self.env = env
207
208        self.exists = False
209
210        self.realm = realm
211
212        tmmodelprovider = GenericClassModelProvider(self.env)
213
214        self.fields = tmmodelprovider.get_fields(realm)
215        self.time_fields = [f['name'] for f in self.fields
216                            if f['type'] == 'time']
217
218        self.metadata = tmmodelprovider.get_metadata(realm)
219
220        if key is not None and len(key) > 0:
221            self.key = key
222            self.resource = Resource(realm, self.gey_key_string())
223        else:
224            self.resource = None
225
226        if not key or not self._fetch_object(key, db):
227            self._init_defaults(db)
228            self.exists = False
229
230        self.env.log.debug("Exists: %s" % self.exists)
231        self.env.log.debug(self.values)
232
233        self._old = {}
234
235    def get_key_prop_names(self):
236        """
237        Returns an array with the fields representing the identity
238        of this object.
239        The specified fields are assumed being also part of the
240        self.fields array.
241        The specified fields are also assumed to correspond to
242        columns with same name in the database table.
243        """
244        return ['id']
245
246    def get_key_prop_values(self):
247        """
248        Returns an array of values for the properties returned by
249        get_key_prop_names.
250        """
251        result = []
252
253        for f in self.get_key_prop_names():
254            result.append(self.values[f])
255
256        return result
257
258    def get_resource_id(self):
259        """
260        Returns a string representation of the object's identity.
261        Used with the trac Resource API.
262        """
263        return [str(self.values[f])+'|' for f in self.get_key_prop_names()]
264
265    def _init_defaults(self, db=None):
266        """
267        Initializes default values for a new object, based on
268        default values specified in the trac.ini file.
269        """
270        for field in self.fields:
271            default = None
272            if field['name'] in self.protected_fields:
273                # Ignore for new - only change through workflow
274                pass
275            elif not field.get('custom'):
276                default = self.env.config.get(self.realm,
277                                              'default_' + field['name'])
278            else:
279                default = field.get('value')
280                options = field.get('options')
281                if default and options and default not in options:
282                    try:
283                        default = options[int(default)]
284                    except (ValueError, IndexError):
285                        self.env.log.warning('Invalid default value "%s" '
286                                             'for custom field "%s"'
287                                             % (default, field['name']))
288            if default:
289                self.values.setdefault(field['name'], default)
290
291    def _fetch_object(self, key, db=None):
292        self.env.log.debug('>>> _fetch_object')
293
294        if not db:
295            db = self.env.get_read_db()
296
297        if not self.pre_fetch_object(db):
298            self.env.log.debug('<<< _fetch_object (pre_fetch_object returned False)')
299            return
300
301        row = None
302
303        # Fetch the standard fields
304        std_fields = [f['name'] for f in self.fields
305                      if not f.get('custom')]
306        cursor = db.cursor()
307
308        sql_where = "WHERE 1=1"
309        for k in self.get_key_prop_names():
310            sql_where += " AND " + k + "=%%s"
311
312        self.env.log.debug("Searching for %s: %s" % (self.realm, sql_where))
313        for k in self.get_key_prop_names():
314            self.env.log.debug("%s = %s" % (k, self[k]))
315
316        cursor.execute(("SELECT %s FROM %s " + sql_where)
317                       % (','.join(std_fields), self.realm), self.get_key_prop_values())
318        row = cursor.fetchone()
319
320        if not row:
321            #raise ResourceNotFound(_('The specified object of type %(realm)s does not exist.',
322            #                         realm=self.realm), _('Invalid object key'))
323            self.env.log.debug("Object NOT found.")
324            return False
325
326        self.env.log.debug("Object found.")
327
328        self.key = self.build_key_object()
329        for i, field in enumerate(std_fields):
330            value = row[i]
331            if field in self.time_fields:
332                self.values[field] = from_any_timestamp(value)
333            # Cinc: we don't want to have '0' in text fields
334            # elif value is None:
335            #     self.values[field] = '0'
336            else:
337                self.values[field] = value
338
339        # Fetch custom fields if available
340        custom_fields = [f['name'] for f in self.fields if f.get('custom')]
341        if len(custom_fields) > 0:
342            cursor.execute(("SELECT name,value FROM %s_custom " + sql_where)
343                           % self.realm, self.get_key_prop_values())
344
345            for name, value in cursor:
346                if name in custom_fields:
347                    if value is None:
348                        self.values[name] = '0'
349                    else:
350                        self.values[name] = value
351
352        self.post_fetch_object(db)
353
354        self.exists = True
355
356        self.env.log.debug('<<< _fetch_object')
357        return True
358
359    def build_key_object(self):
360        """
361        Builds and returns a dictionary object with the key properties,
362        as returned by get_key_prop_names.
363        """
364        key = None
365        for k in self.get_key_prop_names():
366            if (self.values[k] is not None):
367                if key is None:
368                    key = {}
369
370                key[k] = self.values[k]
371
372        return key
373
374    def gey_key_string(self):
375        """
376        Returns a JSON string with the object key properties
377        """
378        return get_string_from_dictionary(self.key)
379
380    def get_values_as_string(self, props):
381        """
382        Returns a JSON string for the specified object properties.
383
384        :param props: An array of field names.
385        """
386        return get_string_from_dictionary(props, self.values)
387
388    def __getitem__(self, name):
389        """
390        Allows for using the syntax "obj['fieldname']" to access this
391        object's values.
392        """
393        return self.values.get(name)
394
395    def __setitem__(self, name, value):
396        """
397        Allows for using the syntax "obj['fieldname']" to access this
398        object's values.
399        Also logs object modifications so the table <realm>_change
400        can be updated.
401        """
402        if name in self.values:
403            self.env.log.debug("Value before: %s" % self.values[name])
404
405        if name in self.values and self.values[name] == value:
406            return
407        if name not in self._old: # Changed field
408            self.env.log.debug("Changing '%s' field value." % name)
409            self._old[name] = self.values.get(name)
410        elif self._old[name] == value: # Change of field reverted
411            del self._old[name]
412        if value:
413            if isinstance(value, list):
414                raise TracError(_("Multi-values fields not supported yet"))
415            field = [field for field in self.fields if field['name'] == name]
416            if field and field[0].get('type') == 'text':
417                value = value.strip()
418        self.values[name] = value
419        self.env.log.debug("Value after: %s" % self.values[name])
420
421    def get_value_or_default(self, name):
422        """
423        Return the value of a field or the default value if it is undefined
424        """
425        try:
426            value = self.values[name]
427            if value is not '0':
428                return value
429            field = [field for field in self.fields if field['name'] == name]
430            if field:
431                return field[0].get('value', '')
432        except KeyError:
433            pass
434
435    def populate(self, values):
436        """
437        Populate the object with 'suitable' values from a dictionary
438        """
439        field_names = [f['name'] for f in self.fields]
440        for name in [name for name in values.keys() if name in field_names]:
441            self[name] = values.get(name, '')
442
443        # We have to do an extra trick to catch unchecked checkboxes
444        for name in [name for name in values.keys() if name[9:] in field_names
445                     and name.startswith('checkbox_')]:
446            if name[9:] not in values:
447                self[name[9:]] = '0'
448
449    def insert(self, when=None, db=None):
450        """
451        Add object to database.
452
453        Parameters:
454            When: a datetime object to specify a creation date.
455
456        The `db` argument is deprecated in favor of `with_transaction()`.
457        """
458        self.env.log.debug('>>> insert')
459
460        assert not self.exists, 'Cannot insert an existing object'
461
462        @self.env.with_transaction(db)
463        def do_insert(db):
464            if not self.pre_insert(db):
465                self.env.log.debug('<<< insert (pre_insert returned False)')
466                return
467
468            t_when = when
469
470            # Add a timestamp
471            if t_when is None:
472                t_when = datetime.now(utc)
473            self.values['time'] = self.values['changetime'] = t_when
474
475            # Perform type conversions
476            self.env.log.debug('  Performing type conversions')
477            values = dict(self.values)
478            for field in self.time_fields:
479                if field in values:
480                    values[field] = to_any_timestamp(values[field])
481
482            # Insert record
483            self.env.log.debug('  Getting fields')
484            std_fields = []
485            custom_fields = []
486            for f in self.fields:
487                fname = f['name']
488                if fname in self.values:
489                    if f.get('custom'):
490                        custom_fields.append(fname)
491                    else:
492                        std_fields.append(fname)
493
494            self.env.log.debug('  Inserting record')
495            cursor = db.cursor()
496            cursor.execute("INSERT INTO %s (%s) VALUES (%s)"
497                           % (self.realm,
498                              ','.join(std_fields),
499                              ','.join(['%s'] * len(std_fields))),
500                           [values[name] for name in std_fields])
501
502            # Cinc: Make sure object has id in instance variable
503            id_ = db.get_last_id(cursor, self.realm)
504            for item in self.get_key_prop_names():
505                if not self[item]:
506                    self[item] = id_
507            # End Cinc
508
509            # Insert custom fields
510            key_names = self.get_key_prop_names()
511            key_values = self.get_key_prop_values()
512            if len(custom_fields) > 0:
513                self.env.log.debug('  Inserting custom fields')
514                cursor.executemany("""
515                INSERT INTO %s_custom (%s,name,value) VALUES (%s,%%s,%%s)
516                """
517                % (self.realm,
518                   ','.join(key_names),
519                   ','.join(['%s'] * len(key_names))),
520                [to_list((key_values, name, self[name])) for name in custom_fields])
521
522            self.post_insert(db)
523
524        self.env.log.debug('  Setting up internal fields')
525        self.exists = True
526
527        # Cinc: this was skipped during init when only 'id' is given as a key property. 'id' is None for new objects
528        self.key = self.build_key_object()
529        self.resource = Resource(self.realm, self.gey_key_string())
530        self._old = {}
531
532        self.env.log.debug('  Calling listeners')
533        from codereview.tracgenericclass.api import GenericClassSystem
534        for listener in GenericClassSystem(self.env).change_listeners:
535            listener.object_created(self.realm, self)
536
537        self.env.log.debug('<<< insert')
538        return self.key
539
540    def save_changes(self, author=None, comment=None, when=None, db=None, cnum=''):
541        """
542        Store object changes in the database. The object must already exist in
543        the database.  Returns False if there were no changes to save, True
544        otherwise.
545
546        The `db` argument is deprecated in favor of `with_transaction()`.
547        """
548        self.env.log.debug('>>> save_changes')
549        assert self.exists, 'Cannot update a new object'
550
551        if not self._old and not comment:
552            return False # Not modified
553
554        if when is None:
555            when = datetime.now(utc)
556        when_ts = to_any_timestamp(when)
557
558        @self.env.with_transaction(db)
559        def do_save_changes(db):
560            if not self.pre_save_changes(db):
561                self.env.log.debug('<<< save_changes (pre_save_changes returned False)')
562                return
563
564            cursor = db.cursor()
565
566            # store fields
567            custom_fields = [f['name'] for f in self.fields if f.get('custom')]
568
569            key_names = self.get_key_prop_names()
570            key_values = self.get_key_prop_values()
571            sql_where = '1=1'
572            for k in key_names:
573                sql_where += " AND " + k + "=%%s"
574
575            for name in self._old.keys():
576                if name in custom_fields:
577                    cursor.execute(("""
578                        SELECT * FROM %s_custom
579                        WHERE name=%%s AND
580                        """ + sql_where) % self.realm, to_list((name, key_values)))
581
582                    if cursor.fetchone():
583                        cursor.execute(("""
584                            UPDATE %s_custom SET value=%%s
585                            WHERE name=%%s AND
586                            """ + sql_where) % self.realm, to_list((self[name], name, key_values)))
587                    else:
588                        cursor.execute("""
589                            INSERT INTO %s_custom (%s,name,value)
590                            VALUES (%s,%%s,%%s)
591                            """
592                            % (self.realm,
593                            ','.join(key_names),
594                            ','.join(['%s'] * len(key_names))),
595                            to_list((key_values, name, self[name])))
596                else:
597                    cursor.execute(("""
598                        UPDATE %s SET %s=%%s WHERE
599                        """ + sql_where)
600                        % (self.realm, name),
601                        to_list((self[name], key_values)))
602
603                if self.metadata['has_change']:
604                    cursor.execute(("""
605                        INSERT INTO %s_change
606                            (%s, time,author,field,oldvalue,newvalue)
607                        VALUES (%s, %%s, %%s, %%s, %%s, %%s)
608                        """
609                        % (self.realm,
610                        ','.join(key_names),
611                        ','.join(['%s'] * len(key_names)))),
612                        to_list((key_values, when_ts, author, name,
613                        self._old[name], self[name])))
614
615            self.post_save_changes(db)
616
617        old_values = self._old
618        self._old = {}
619        self.values['changetime'] = when
620
621        from codereview.tracgenericclass.api import GenericClassSystem
622        for listener in GenericClassSystem(self.env).change_listeners:
623            listener.object_changed(self.realm, self, comment, author, old_values)
624
625        self.env.log.debug('<<< save_changes')
626        return True
627
628    def delete(self, db=None):
629        """
630        Delete the object. Also clears the change history and the
631        custom fields.
632
633        The `db` argument is deprecated in favor of `with_transaction()`.
634        """
635
636        self.env.log.debug('>>> delete')
637
638        @self.env.with_transaction(db)
639        def do_delete(db):
640            if not self.pre_delete(db):
641                self.env.log.debug('<<< delete (pre_delete returned False)')
642                return
643
644            #Attachment.delete_all(self.env, 'ticket', self.id, db)
645
646            cursor = db.cursor()
647
648            key_names = self.get_key_prop_names()
649            key_values = self.get_key_prop_values()
650
651            sql_where = 'WHERE 1=1'
652            for k in key_names:
653                sql_where += " AND " + k + "=%%s"
654
655            self.env.log.debug("Deleting %s: %s" % (self.realm, sql_where))
656            for k in key_names:
657                self.env.log.debug("%s = %s" % (k, self[k]))
658
659            cursor.execute(("DELETE FROM %s " + sql_where)
660                % self.realm, key_values)
661
662            if self.metadata['has_change']:
663                cursor.execute(("DELETE FROM %s_change " + sql_where)
664                    % self.realm, key_values)
665
666            if self.metadata['has_custom']:
667                custom_fields = [f['name'] for f in self.fields if f.get('custom')]
668                if len(custom_fields) > 0:
669                    cursor.execute(("DELETE FROM %s_custom " + sql_where)
670                        % self.realm, key_values)
671
672            self.post_delete(db)
673
674        from codereview.tracgenericclass.api import GenericClassSystem
675        for listener in GenericClassSystem(self.env).change_listeners:
676            listener.object_deleted(self.realm, self)
677
678        self.exists = False
679        self.env.log.debug('<<< delete')
680
681    def save_as(self, new_key, when=None, db=None):
682        """
683        Saves (a copy of) the object with different key.
684        The previous object is not deleted, so if needed it must be
685        deleted explicitly.
686        """
687        self.env.log.debug('>>> save_as')
688
689        @self.env.with_transaction(db)
690        def do_save_as(db):
691            old_key = self.key
692            if not self.pre_save_as(old_key, new_key, db):
693                self.env.log.debug('<<< save_as (pre_save_as returned False)')
694                return
695
696            self.key = new_key
697
698            # Copy values from key into corresponding self.values field
699            for f in self.get_key_prop_names():
700                self.values[f] = new_key[f]
701
702            self.exists = False
703
704            # Create object with new key
705            self.insert(when, db)
706
707            self.post_save_as(old_key, new_key, db)
708
709        self.env.log.debug('<<< save_as')
710
711    def list_change_history(self, db=None):
712        """
713        Returns an ordered list of all the changes to standard and
714        custom field, with the old and new value, along with timestamp
715        and author, starting from the most recent.
716        """
717        self.env.log.debug('>>> list_change_history')
718
719        if self.metadata['has_change']:
720            std_fields = [f['name'] for f in self.fields
721                          if not f.get('custom')]
722
723            sql_where = "WHERE 1=1"
724            for k in self.get_key_prop_names():
725                sql_where += " AND " + k + "=%%s"
726
727            if not db:
728                db = self.env.get_read_db()
729
730            cursor = db.cursor()
731
732            cursor.execute(("SELECT time,author,field,oldvalue,newvalue FROM %s_change " + sql_where+ " ORDER BY time DESC")
733                           % self.realm, self.get_key_prop_values())
734
735            for ts, author, fname, oldvalue, newvalue in cursor:
736                yield ts, author, fname, oldvalue, newvalue
737
738        self.env.log.debug('<<< list_change_history')
739
740    def get_non_empty_prop_names(self):
741        """
742        Returns a list of names of the fields that are not None.
743        """
744        std_field_names = []
745        custom_field_names = []
746
747        for field in self.fields:
748            n = field.get('name')
749
750            if n in self.values and self.values[n] is not None:
751                if not field.get('custom'):
752                    std_field_names.append(n)
753                else:
754                    custom_field_names.append(n)
755
756        return std_field_names, custom_field_names
757
758    def get_values(self, prop_names):
759        """
760        Returns a list of the values for the specified properties,
761        in the same order as the property names.
762        """
763        result = []
764
765        for n in prop_names:
766            result.append(self.values[n])
767
768        return result
769
770    def set_values(self, props):
771        """
772        Sets multiple properties into this object.
773
774        Note: this method does not keep history of property changes.
775        """
776        for n in props:
777            self.values[n] = props[n]
778
779    def _get_key_from_row(self, row):
780        """
781        Given a database row with the key properties, builds a
782        dictionary with this object's key.
783        """
784        key = {}
785
786        for i, f in enumerate(self.get_key_prop_names()):
787            key[f] = row[i]
788
789        return key
790
791    def create_instance(self, key):
792        """
793        Subclasses should override this method to create an instance
794        of them with the specified key.
795        """
796        pass
797
798    def list_matching_objects(self, exact_match=True, operator=None, db=None):
799        """
800        List the objects that match the current values of this object's
801        fields.
802        To use this method, first create an instance with no key, then
803        fill some of its fields with the values you want to find a
804        match on, then call this method.
805        A collection of objects found in the database matching the
806        fields you had provided values for will be returned.
807        An exact match, i.e. an SQL '=' operator, will be used, unless you
808        specify exact_match=False, in which case the SQL 'LIKE' operator
809        will be used.
810
811        The `db` argument is deprecated in favor of `with_transaction()`.
812        """
813        self.env.log.debug('>>> list_matching_objects')
814
815        if not db:
816            db = self.env.get_read_db()
817
818        self.pre_list_matching_objects(db)
819
820        cursor = db.cursor()
821
822        non_empty_std_names, non_empty_custom_names = self.get_non_empty_prop_names()
823
824        non_empty_std_values = self.get_values(non_empty_std_names)
825        non_empty_custom_values = self.get_values(non_empty_custom_names)
826
827        if operator == None:
828            operator = '='
829            if not exact_match:
830                operator = ' LIKE '
831
832        sql_where = '1=1'
833        for k in non_empty_std_names:
834            sql_where += " AND " + k + operator + '%%s'
835
836        cursor.execute(('SELECT %s FROM %s WHERE ' + sql_where)
837                       % (','.join(self.get_key_prop_names()), self.realm),
838                       non_empty_std_values)
839
840        for row in cursor:
841            key = self._get_key_from_row(row)
842            self.env.log.debug('<<< list_matching_objects - returning result')
843            yield self.create_instance(key)
844
845        self.env.log.debug('<<< list_matching_objects')
846
847    def get_search_results(self, req, terms, filters):
848        """
849        Called in the context of the trac search API, to return a list
850        of objects of this class matching the specified terms.
851
852        Concrete classes should override this method to perform class-specific
853        searches.
854        """
855        if False:
856            yield None
857
858    # Following is a set of callbacks allowing subclasses to perform
859    # actions around the operations that pertain the lifecycle of
860    # this object.
861
862    def pre_fetch_object(self, db):
863        """
864        Use this method to perform initialization before fetching the
865        object from the database.
866        Return False to prevent the object from being fetched from the
867        database.
868        """
869        return True
870
871    def post_fetch_object(self, db):
872        """
873        Use this method to further fulfill your object after being
874        fetched from the database.
875        """
876        pass
877
878    def pre_insert(self, db):
879        """
880        Use this method to perform work before inserting the
881        object into the database.
882        Return False to prevent the object from being inserted into the
883        database.
884        """
885        return True
886
887    def post_insert(self, db):
888        """
889        Use this method to perform further work after your object has
890        been inserted into the database.
891
892        You should throw an exception inside here if you want the insert
893        to be aborted (i.e. all the work done so far rolled back).
894        """
895        pass
896
897    def pre_save_changes(self, db):
898        """
899        Use this method to perform work before saving the object changes
900        into the database.
901        Return False to prevent the object changes from being saved into
902        the database.
903        """
904        return True
905
906    def post_save_changes(self, db):
907        """
908        Use this method to perform further work after your object
909        changes have been saved into the database.
910        """
911        pass
912
913    def pre_delete(self, db):
914        """
915        Use this method to perform work before deleting the object from
916        the database.
917        Return False to prevent the object from being deleted from the
918        database.
919        """
920        return True
921
922    def post_delete(self, db):
923        """
924        Use this method to perform further work after your object
925        has been deleted from the database.
926        """
927        pass
928
929    def pre_save_as(self, old_key, new_key, db):
930        """
931        Use this method to perform work before saving the object with
932        a different identity into the database.
933        Return False to prevent the object from being saved into the
934        database.
935        """
936        return True
937
938    def post_save_as(self, old_key, new_key, db):
939        """
940        Use this method to perform further work after your object
941        has been saved into the database.
942        """
943        pass
944
945    def pre_list_matching_objects(self, db):
946        """
947        Use this method to perform work before finding matches in the
948        database.
949        Return False to prevent the search.
950        """
951        return True
952
953
954class AbstractWikiPageWrapper(AbstractVariableFieldsObject):
955    """
956    This subclass is a generic object that is based on a wiki page,
957    identified by the 'page_name' field.
958    The wiki page lifecycle is managed along with the normal object's
959    one.
960    """
961    def __init__(self, env, realm='wiki_wrapper_obj', key=None, db=None):
962        AbstractVariableFieldsObject.__init__(self, env, realm, key, db)
963
964    def post_fetch_object(self, db):
965        self.wikipage = WikiPage(self.env, self.values['page_name'])
966
967    def delete(self, del_wiki_page=True, db=None):
968        """
969        Delete the object. Also deletes the Wiki page if so specified in the parameters.
970
971        The `db` argument is deprecated in favor of `with_transaction()`.
972        """
973
974        # The actual wiki page deletion is delayed until pre_delete.
975        self.del_wiki_page = del_wiki_page
976
977        AbstractVariableFieldsObject.delete(self, db)
978
979    def pre_insert(self, db):
980        """
981        Assuming the following fields have been given a value before this call:
982        text, author, remote_addr, values['page_name']
983        """
984
985        wikipage = WikiPage(self.env, self.values['page_name'])
986        wikipage.text = self.text
987        wikipage.save(self.author, '', self.remote_addr)
988
989        self.wikipage = wikipage
990
991        return True
992
993    def pre_save_changes(self, db):
994        """
995        Assuming the following fields have been given a value before this call:
996        text, author, remote_addr, values['page_name']
997        """
998
999        wikipage = WikiPage(self.env, self.values['page_name'])
1000        wikipage.text = self.text
1001        wikipage.save(self.author, '', self.remote_addr)
1002
1003        self.wikipage = wikipage
1004
1005        return True
1006
1007    def pre_delete(self, db):
1008        """
1009        Assuming the following fields have been given a value before this call:
1010        values['page_name']
1011        """
1012
1013        if self.del_wiki_page:
1014            wikipage = WikiPage(self.env, self.values['page_name'])
1015            wikipage.delete()
1016
1017        self.wikipage = None
1018
1019        return True
1020
1021
1022    def get_search_results(self, req, terms, filters):
1023        """
1024        Currently delegates the search to the Wiki module.
1025        """
1026        for result in WikiModule(self.env).get_search_results(req, terms, ('wiki',)):
1027            yield result
1028
1029
1030class GenericClassModelProvider(Component):
1031    """
1032    This class provides a factory for generic classes and derivatives.
1033
1034    The actual data model on the db is created starting from the
1035    SCHEMA declaration below.
1036    For each table, we specify whether to create also a '_custom' and
1037    a '_change' table.
1038
1039    This class also provides the specification of the available fields
1040    for each class, being them standard fields and the custom fields
1041    specified in the trac.ini file.
1042    The custom field specification follows the same syntax as for
1043    Tickets.
1044    Currently, only 'text' type of fields are supported.
1045    """
1046
1047    class_providers = ExtensionPoint(IConcreteClassProvider)
1048
1049    all_fields = {}
1050    all_custom_fields = {}
1051    all_metadata = {}
1052
1053    _class_providers_map = None
1054
1055    # Class providers managament
1056    def get_class_provider(self, realm):
1057        """
1058        Return the component responsible for providing the specified
1059        concrete class implementation.
1060
1061        :param  realm: the realm which uniquely identifies the class.
1062        :return: a `Component` implementing `IConcreteClassProvider`
1063                 or `None`
1064        """
1065        # build a dict of realm keys to IConcreteClassProvider
1066        # implementations
1067        if not self._class_providers_map:
1068            map = {}
1069            for provider in self.class_providers:
1070                for r in provider.get_realms() or []:
1071                    self.env.log.debug("Mapping realm %s to provider %s" % (r, provider))
1072                    map[r] = provider
1073            self._class_providers_map = map
1074
1075        if realm in self._class_providers_map:
1076            return self._class_providers_map.get(realm)
1077        else:
1078            return None
1079
1080    def get_known_realms(self):
1081        """
1082        Return a list of all the realm names of registered
1083        class providers.
1084        """
1085        realms = []
1086        for provider in self.class_providers:
1087            for realm in provider.get_realms() or []:
1088                realms.append(realm)
1089
1090        return realms
1091
1092
1093    # Factory method
1094    def get_object(self, realm, key=None):
1095        """
1096        Returns an instance of the specified class (by means of its
1097        realm name), with the specified key.
1098        """
1099        obj = None
1100
1101        provider = self.get_class_provider(realm)
1102        self.env.log.debug("Provider for realm %s is %s" % (realm, provider))
1103
1104        if provider:
1105            self.env.log.debug("Object key is %s" % key)
1106            return provider.create_instance(realm, key)
1107        else:
1108            self.env.log.debug("Provider for realm %s not found" % realm)
1109            return None
1110
1111
1112    # Permission check
1113    def check_permission(self, req, realm, key_str=None, operation='set', name=None, value=None):
1114        """
1115        Checks whether the logged in User has permission to perform
1116        the specified operation on a resource of the specified realm and
1117        optionally with the specified key.
1118
1119        Raise an exception if authorization is denied.
1120
1121        Actually delegates to the concrete class provider the permission check.
1122
1123        See the IConcreteClassProvider method with the same name for more details
1124        about the available operations and the function parameters.
1125        """
1126
1127        provider = self.get_class_provider(realm)
1128        if provider is not None:
1129            provider.check_permission(req, realm, key_str, operation, name, value)
1130
1131
1132    # Fields management
1133    def reset_fields(self):
1134        """
1135        Invalidate field cache.
1136        """
1137        self.all_fields = {}
1138
1139    def get_fields(self, realm):
1140        self.env.log.debug(">>> get_fields")
1141
1142        if realm not in self.fields():
1143            raise TracError("Requested field information not found for class %s." % realm)
1144
1145        fields = copy.deepcopy(self.fields()[realm])
1146        #label = 'label' # workaround gettext extraction bug
1147        #for f in fields:
1148        #    f[label] = gettext(f[label])
1149
1150        self.env.log.debug("<<< get_fields")
1151        return fields
1152
1153    def get_metadata(self, realm):
1154        tmp_metadata = self.metadata()
1155        if realm in tmp_metadata:
1156            metadata = copy.deepcopy(tmp_metadata[realm])
1157        else:
1158            metadata = None
1159
1160        return metadata
1161
1162    def fields(self, refresh=False):
1163        """Return the list of fields available for every realm."""
1164
1165        if refresh or not self.all_fields:
1166            fields = {}
1167
1168            for provider in self.class_providers:
1169                realm_fields = provider.get_fields()
1170                for realm in realm_fields:
1171                    tmp_fields = realm_fields[realm]
1172
1173                    self.append_custom_fields(tmp_fields, self.get_custom_fields_for_realm(realm))
1174
1175                    fields[realm] = tmp_fields
1176
1177            self.all_fields = fields
1178
1179            # Print debug information about all known realms and fields
1180            for r in self.all_fields:
1181                self.env.log.debug("Fields for realm %s:" % r)
1182                for f in self.all_fields[r]:
1183                    self.env.log.debug("   %s : %s" % (f['name'], f['type']))
1184                    if 'custom' in f:
1185                        self.env.log.debug("     (custom)")
1186
1187        return self.all_fields
1188
1189    def metadata(self):
1190        """Return metadata information about concrete classes."""
1191
1192        if not self.all_metadata:
1193            metadata = {}
1194
1195            for provider in self.class_providers:
1196                realm_metadata = provider.get_metadata()
1197                for realm in realm_metadata:
1198                    metadata[realm] = realm_metadata[realm]
1199
1200            self.all_metadata = metadata
1201
1202        return self.all_metadata
1203
1204    def append_custom_fields(self, fields, custom_fields):
1205        if len(custom_fields) > 0:
1206            for f in custom_fields:
1207                fields.append(f)
1208
1209    def get_custom_fields_for_realm(self, realm):
1210        fields = []
1211
1212        for field in self.get_custom_fields(realm):
1213            field['custom'] = True
1214            fields.append(field)
1215
1216        return fields
1217
1218    def get_custom_fields(self, realm):
1219        return copy.deepcopy(self.custom_fields(realm))
1220
1221    def custom_fields(self, realm, refresh=False):
1222        """Return the list of available custom fields."""
1223
1224        if refresh or not realm in self.all_custom_fields:
1225            fields = []
1226            config = self.config[realm+'-tm_custom']
1227
1228            self.env.log.debug(config.options())
1229
1230            for name in [option for option, value in config.options()
1231                         if '.' not in option]:
1232                if not re.match('^[a-zA-Z][a-zA-Z0-9_]+$', name):
1233                    self.log.warning('Invalid name for custom field: "%s" '
1234                                     '(ignoring)', name)
1235                    continue
1236
1237                self.env.log.debug("  Option: %s" % name)
1238
1239                field = {
1240                    'name': name,
1241                    'type': config.get(name),
1242                    'order': config.getint(name + '.order', 0),
1243                    'label': config.get(name + '.label') or name.capitalize(),
1244                    'value': config.get(name + '.value', '')
1245                }
1246                if field['type'] == 'select' or field['type'] == 'radio':
1247                    field['options'] = config.getlist(name + '.options', sep='|')
1248                    if '' in field['options']:
1249                        field['optional'] = True
1250                        field['options'].remove('')
1251                elif field['type'] == 'text':
1252                    field['format'] = config.get(name + '.format', 'plain')
1253                elif field['type'] == 'textarea':
1254                    field['format'] = config.get(name + '.format', 'plain')
1255                    field['cols'] = config.getint(name + '.cols')
1256                    field['rows'] = config.getint(name + '.rows')
1257                fields.append(field)
1258
1259            fields.sort(lambda x, y: cmp(x['order'], y['order']))
1260
1261            self.all_custom_fields[realm] = fields
1262
1263        return self.all_custom_fields[realm]
1264
1265
1266# Methods to help components create their databases
1267def need_db_create_for_realm(env, realm, realm_metadata, db=None):
1268    """
1269    Call this method from inside your Component IEnvironmentSetupParticipant's
1270    environment_needs_upgrade() function to check whether your Component
1271    using the generic classes needs to create the corresponding database tables.
1272
1273    :param realm_metadata: The db table metadata that, if missing, means that the
1274                database must be created.
1275    """
1276    current_version = _get_installed_version(env, realm, db)
1277    env.log.debug("Current database version for class '%s' is %s", realm, current_version)
1278
1279    if current_version is None or current_version <= 0:
1280        env.log.info("Need to create db tables for class '%s'.", realm)
1281        return True
1282
1283    env.log.debug("No need to create database for class \'%s\'.", realm)
1284
1285    return False
1286
1287def need_db_upgrade_for_realm(env, realm, realm_schema, db=None):
1288    """
1289    Call this method from inside your Component IEnvironmentSetupParticipant's
1290    environment_needs_upgrade() function to check whether your Component
1291    using the generic classes needs to update the corresponding database tables.
1292
1293    :param realm_schema: The db schema definition, as returned by
1294                   the get_data_models() function in the IConcreteClassProvider
1295                   interface.
1296    """
1297
1298    table_metadata = realm_schema['table']
1299    desired_version = realm_schema['version']
1300
1301    current_version = _get_installed_version(env, realm, db)
1302    env.log.debug("Current database version for class '%s' is %s. Desired version is %s", realm, current_version, desired_version)
1303
1304    if current_version is None or current_version < desired_version:
1305        env.log.info("Need to update db tables for class '%s'.", realm)
1306        return True
1307
1308    env.log.debug("No need to update database for class \'%s\'.", realm)
1309
1310    return False
1311
1312def create_db_for_realm(env, realm, realm_schema, db=None):
1313    """
1314    Call this method from inside your Component IEnvironmentSetupParticipant's
1315    upgrade_environment() function to create the database tables corresponding to
1316    your Component's generic classes.
1317
1318    :param realm_schema: The db schema definition, as returned by
1319                   the get_data_models() function in the IConcreteClassProvider
1320                   interface.
1321    """
1322    @env.with_transaction(db)
1323    def do_create_db_for_realm(db):
1324        cursor = db.cursor()
1325
1326        db_backend, _ = DatabaseManager(env).get_connector()
1327
1328        env.log.info("Creating DB for class '%s'.", realm)
1329
1330        # Create the required tables
1331        table_metadata = realm_schema['table']
1332        version = realm_schema['version']
1333        tablename = table_metadata.name
1334
1335        key_names = [k for k in table_metadata.key]
1336
1337        # Create base table
1338        env.log.info("Creating base table %s...", tablename)
1339        for stmt in db_backend.to_sql(table_metadata):
1340            env.log.debug(stmt)
1341            cursor.execute(stmt)
1342
1343        # Create custom fields table if required
1344        if realm_schema['has_custom']:
1345            cols = []
1346            for k in key_names:
1347                # Determine type of column k
1348                type = 'text'
1349                for c in table_metadata.columns:
1350                    if c.name == k:
1351                        type = c.type
1352
1353                cols.append(Column(k, type=type))
1354
1355            cols.append(Column('name'))
1356            cols.append(Column('value'))
1357
1358            custom_key = copy.deepcopy(key_names)
1359            custom_key.append('name')
1360
1361            table_custom = Table(tablename+'_custom', key = custom_key)[cols]
1362            env.log.info("Creating custom properties table %s...", table_custom.name)
1363            for stmt in db_backend.to_sql(table_custom):
1364                env.log.debug(stmt)
1365                cursor.execute(stmt)
1366
1367        # Create change history table if required
1368        if realm_schema['has_change']:
1369            cols = []
1370            for k in key_names:
1371                # Determine type of column k
1372                type = 'text'
1373                for c in table_metadata.columns:
1374                    if c.name == k:
1375                        type = c.type
1376
1377                cols.append(Column(k, type=type))
1378
1379            cols.append(Column('time', type=get_timestamp_db_type()))
1380            cols.append(Column('author'))
1381            cols.append(Column('field'))
1382            cols.append(Column('oldvalue'))
1383            cols.append(Column('newvalue'))
1384            cols.append(Index(key_names))
1385
1386            change_key = copy.deepcopy(key_names)
1387            change_key.append('time')
1388            change_key.append('field')
1389
1390            table_change = Table(tablename+'_change', key = change_key)[cols]
1391            env.log.info("Creating change history table %s...", table_change.name)
1392            for stmt in db_backend.to_sql(table_change):
1393                env.log.debug(stmt)
1394                cursor.execute(stmt)
1395
1396        _set_installed_version(env, realm, version, db)
1397
1398def upgrade_db_for_realm(env, package_name, realm, realm_schema, db=None):
1399    """
1400    Each db version should have its own upgrade module, named
1401    upgrades/db_<schema>_<N>.py, where 'N' is the version number (int).
1402    """
1403    @env.with_transaction(db)
1404    def do_upgrade_db_for_realm(db):
1405        cursor = db.cursor()
1406
1407        db_backend = DatabaseManager(env).get_connector()[0]
1408
1409        env.log.info("Upgrading DB for class '%s'.", realm)
1410
1411        # Create the required tables
1412        table_metadata = realm_schema['table']
1413        version = realm_schema['version']
1414        tablename = table_metadata.name
1415
1416        cursor = db.cursor()
1417        current_version = _get_installed_version(env, realm, db)
1418
1419        for i in range(current_version + 1, version + 1):
1420            env.log.info('Upgrading database version for class \'%s\' from %d to %d', realm, i - 1, i)
1421
1422            name  = 'db_%s_%i' % (realm, i)
1423            try:
1424                upgrades = __import__(package_name, globals(), locals(), [name])
1425                script = getattr(upgrades, name)
1426            except AttributeError:
1427                raise TracError(_('No upgrade module for version %(num)i '
1428                                  '(%(version)s.py)', num=i, version=name))
1429            script.do_upgrade(env, i, db_backend, db)
1430
1431            _set_installed_version(env, realm, i, db)
1432
1433            env.log.info('Upgrade step successful.')
1434
1435
1436# DB schema management methods
1437
1438def _get_installed_version(env, realm, db=None):
1439    """
1440    :return: -1, if the DB for realm does not exist,
1441             a number greater or equals to 1 as the installed DB version for realm.
1442    """
1443    version = _get_system_value(env, realm + '_version', None, db)
1444    if version is None:
1445        # check for old naming schema
1446        dburi = env.config.get('trac', 'database')
1447        env.log.debug('Database backend is \'%s\'', dburi)
1448
1449        tables = list_available_tables(dburi, db.cursor())
1450        if 'tracgenericclassconfig' in tables:
1451            version = db_get_config_property(env, 'tracgenericclassconfig', realm + "_dbversion", db)
1452        else:
1453            if realm in tables:
1454                version = 1
1455
1456    if version is None:
1457        version = -1
1458
1459    return int(version)
1460
1461def _set_installed_version(env, realm, version, db=None):
1462    env.log.info('Setting database version for class \'%s\' to %d', realm, version)
1463    _set_system_value(env, realm + '_version', version, db)
1464
1465# Trac db 'system' table management methods
1466
1467def _get_system_value(env, key, default=None, db=None):
1468    result = default
1469
1470    if not db:
1471        db = env.get_read_db()
1472
1473    cursor = db.cursor()
1474    cursor.execute("SELECT value FROM system WHERE name=%s", (key,))
1475    row = cursor.fetchone()
1476
1477    if row and row[0]:
1478        result = row[0]
1479        env.log.debug('Found system key \'%s\' with value %s', key, result)
1480    else:
1481        env.log.debug('System key \'%s\' not found', key)
1482
1483    env.log.debug('Returning system key \'%s\' with value %s', key, result)
1484    return result
1485
1486def _set_system_value(env, key, value, db=None):
1487    """
1488    Atomic UPSERT (i.e. UPDATE or INSERT) db transaction to save realm DB version.
1489    """
1490    @env.with_transaction(db)
1491    def do_set_system_value(db):
1492        cursor = db.cursor()
1493        cursor.execute(
1494                "UPDATE system SET value=%s WHERE name=%s", (value, key))
1495
1496        cursor.execute("SELECT value FROM system WHERE name=%s", (key,))
1497        if not cursor.fetchone():
1498            cursor.execute(
1499                "INSERT INTO system(name, value) VALUES(%s, %s)", (key, value))
Note: See TracBrowser for help on using the repository browser.