source: tracformsplugin/tags/tracforms-0.4.1/0.11/tracforms/formdb.py

Last change on this file was 10490, checked in by Steffen Hoffmann, 12 years ago

TracFormsPlugin: Release maintenance version 0.4.1 for compatibility with Trac 0.11, closes #9000.

These are changes cherry-picked from trunk and merged into tracforms-0.4 to
establish full compatibility with Trac 0.11 on level with plugin's own claims.

File size: 26.4 KB
Line 
1# -*- coding: utf-8 -*-
2
3import re
4import time
5import unittest
6
7from trac.core import Component, implements
8from trac.db import Column, DatabaseManager, Index, Table
9from trac.resource import Resource
10from trac.search.api import search_to_sql
11
12from api import IFormDBObserver, _
13from compat import json, parse_qs, resource_exists
14from tracdb import DBComponent
15from util import resource_from_page, xml_unescape
16from web_ui import parse_history
17
18
19class FormDBComponent(DBComponent):
20    """Provides form update methods and schema components."""
21
22    implements(IFormDBObserver)
23
24    applySchema = True
25
26    # abstract TracForms update methods
27
28    def get_tracform_ids(self, src, db=None):
29        """Returns all child forms of resource specified by parent realm and
30        parent id as a list of tuples (form_id and corresponding subcontext).
31        """
32        db = self._get_db(db)
33        cursor = db.cursor()
34        cursor.execute("""
35            SELECT  id,
36                    subcontext
37            FROM    forms
38            WHERE   realm=%s
39                AND resource_id=%s
40            """, (src[0], src[1]))
41        ids = []
42        for form_id, subcontext in cursor:
43            ids.append(tuple([int(form_id), subcontext]))
44        return ids
45
46    def get_tracform_meta(self, src, db=None):
47        """
48        Returns the meta information about a form based on a form id (int or
49        long) or context (parent realm, parent id, TracForms subcontext).
50        """
51        db = self._get_db(db)
52        cursor = db.cursor()
53        sql = """
54            SELECT  id,
55                    realm,
56                    resource_id,
57                    subcontext,
58                    author,
59                    time,
60                    keep_history,
61                    track_fields
62            FROM    forms
63            """
64        if not isinstance(src, int):
65            sql += """
66                WHERE   realm=%s
67                    AND resource_id=%s
68                    AND subcontext=%s
69                """
70        else:
71            sql += """
72                WHERE   id=%s
73                """
74        form_id = None
75        realm = None
76        resource_id = None
77        subcontext = None
78        if not isinstance(src, int):
79            realm, resource_id, subcontext = src
80        else:
81            form_id = src
82            src = tuple([src],)
83        cursor.execute(sql, src)
84        return cursor.fetchone() or \
85               (form_id, realm, resource_id, subcontext,
86                None, None, None, None)
87
88    def get_tracform_state(self, src, db=None):
89        db = self._get_db(db)
90        cursor = db.cursor()
91        sql = """
92            SELECT  state
93            FROM    forms
94            """
95        if not isinstance(src, int):
96            sql += """
97                WHERE   realm=%s
98                    AND resource_id=%s
99                    AND subcontext=%s
100                """
101        else:
102            sql += """
103                WHERE   id=%s
104                """
105            src = tuple([src],)
106        cursor.execute(sql, src)
107        row = cursor.fetchone()
108        return row and row[0]
109
110    def save_tracform(self, src, state, author,
111                        base_version=None, keep_history=False,
112                        track_fields=False, db=None):
113        (form_id, realm, resource_id, subcontext, last_updater,
114            last_updated_on, form_keep_history,
115            form_track_fields) = self.get_tracform_meta(src, db=db)
116
117        if form_keep_history is not None:
118            keep_history = form_keep_history
119        old_state = form_id and self.get_tracform_state(form_id) or '{}'
120        if form_track_fields is not None:
121            track_fields = form_track_fields
122
123        if base_version is not None:
124            base_version = int(base_version or 0)
125
126        if ((base_version is None and last_updated_on is None) or
127            (base_version == last_updated_on)):
128            if state != old_state:
129                updated_on = int(time.time())
130                db = self._get_db(db)
131                cursor = db.cursor()
132                if form_id is None:
133                    cursor.execute("""
134                        INSERT INTO forms
135                            (realm, resource_id, subcontext,
136                            state, author, time)
137                            VALUES (%s, %s, %s, %s, %s, %s)
138                        """, (realm, resource_id, subcontext,
139                        state, author, updated_on))
140                    form_id = db.get_last_id(cursor, 'forms') 
141                else:
142                    cursor.execute("""
143                        UPDATE  forms
144                        SET     state=%s,
145                                author=%s,
146                                time=%s
147                        WHERE   id=%s
148                        """, (state, author, updated_on, form_id))
149                    if keep_history:
150                        cursor.execute("""
151                            INSERT INTO forms_history
152                                    (id, time, author, old_state)
153                                    VALUES (%s, %s, %s, %s)
154                            """, (form_id, last_updated_on,
155                                last_updater, old_state))
156                if track_fields:
157                    # Break down old version and new version.
158                    old_fields = json.loads(old_state)
159                    new_fields = json.loads(state or '{}')
160                    updated_fields = []
161                    for field, old_value in old_fields.iteritems():
162                        if new_fields.get(field) != old_value:
163                            updated_fields.append(field)
164                    for field in new_fields:
165                        if old_fields.get(field) is None:
166                            updated_fields.append(field)
167                    self.log.debug('UPDATED: ' + str(updated_fields))
168                    for field in updated_fields:
169                        cursor.execute("""
170                            SELECT  COUNT(*)
171                            FROM    forms_fields
172                            WHERE   id=%s
173                                AND field=%s""", (form_id, field))
174                        if cursor.fetchone()[0] > 0:
175                            cursor.execute("""
176                                UPDATE  forms_fields
177                                    SET author=%s,
178                                        time=%s
179                                WHERE   id=%s
180                                    AND field=%s
181                                """, (author, updated_on, form_id, field))
182                        else:
183                            cursor.execute("""
184                                INSERT INTO forms_fields
185                                        (id, field, author, time)
186                                VALUES  (%s, %s, %s, %s)
187                                """, (form_id, field, author, updated_on))
188                db.commit()
189            else:
190                updated_on = last_updated_on
191                author = last_updater
192            return ((form_id, realm, resource_id, subcontext, state,
193                    author, updated_on),
194                    (form_id, realm, resource_id, subcontext, old_state,
195                    last_updater, last_updated_on))
196        else:
197            raise ValueError(_("Conflict"))
198
199    def get_tracform_history(self, src, db=None):
200        db = self._get_db(db)
201        cursor = db.cursor()
202        if isinstance(src, int):
203            form_id = src
204        else:
205            form_id = self.get_tracform_meta(src, db=db)[0]
206        cursor.execute("""
207            SELECT  author, time, old_state
208            FROM    forms_history
209            WHERE   id=%s
210            ORDER   BY time DESC
211            """, (form_id,))
212        return cursor.fetchall()
213
214    def get_tracform_fields(self, src, db=None):
215        db = self._get_db(db)
216        cursor = db.cursor()
217        if isinstance(src, int):
218            form_id = src
219        else:
220            form_id = self.get_tracform_meta(src, db=db)[0]
221        cursor.execute("""
222            SELECT  field, author, time
223            FROM    forms_fields
224            WHERE   id=%s
225            """, (form_id,))
226        return cursor.fetchall()
227
228    def get_tracform_fieldinfo(self, src, field, db=None):
229        """Retrieve author and time of last change per field."""
230        db = self._get_db(db)
231        cursor = db.cursor()
232        if isinstance(src, int):
233            form_id = src
234        else:
235            form_id = self.get_tracform_meta(src, db=db)[0]
236        cursor.execute("""
237            SELECT  author, time
238            FROM    forms_fields
239            WHERE   id=%s
240                AND field=%s
241            """, (form_id, field))
242        return cursor.fetchone() or (None, None)
243
244    def reset_tracform(self, src, field=None, author=None, step=0, db=None):
245        """Delete method for all TracForms db tables.
246
247        Note, that we only delete recorded values and history here, while
248        the form definition (being part of forms parent resource) is retained.
249        Reset of single fields is not implemented, because this would require
250        cumbersome and error prown history rewriting - not worth the hassle.
251        """
252        db = self._get_db(db)
253        cursor = db.cursor()
254        form_ids = []
255        # identify form_id(s) to reset
256        if isinstance(src, int):
257            form_ids.append(src)
258        elif isinstance(src, tuple) and len(src) == 3:
259            if src[-1] is None:
260                # no subcontext given, reset all forms of the parent resource
261                for form_id in self.get_tracform_ids(src[0], src[1], db=db):
262                    form_ids.append(form_id)
263            else:
264                form_ids.append(self.get_tracform_meta(src, db=db)[0])
265        db = self._get_db(db)
266        cursor = db.cursor()
267        # restore of old values for multiple forms is not meaningful
268        if step == -1 and len(form_ids) == 1:
269            form_id = form_ids[0]
270            now = int(time.time())
271            author, updated_on, old_state = self.get_tracform_history(
272                                            form_id, db=db)[0] or \
273                                            (author, now, '{}')
274            if updated_on == now:
275                # no history recorded, so only form values can be reset
276                step = 0
277            else:
278                # copy last old_state to current
279                cursor.execute("""
280                    UPDATE forms
281                        SET author=%s,
282                            time=%s,
283                            state=%s
284                    WHERE   id=%s
285                    """, (author, updated_on, old_state, form_id))
286                history = []
287                records = self.get_tracform_history(form_id, db=db)
288                for history_author, history_time, old_state in records:
289                    history.append({'author': history_author,
290                                    'time': history_time,
291                                    'old_state': old_state})
292                history = parse_history(history, fieldwise=True)
293                # delete restored history entry
294                cursor.execute("""
295                    DELETE
296                    FROM    forms_history
297                    WHERE   id=%s
298                        AND time=%s
299                    """, (form_id, updated_on))
300                # rollback field info changes
301                for field in history.keys():
302                    changes = history[field]
303                    if len(changes) > 0:
304                        # restore last field info, unconditional by intention
305                        # i.e. to not create entries, if track_fields is False
306                        cursor.execute("""
307                            UPDATE  forms_fields
308                                SET author=%s,
309                                    time=%s
310                            WHERE   id=%s
311                                AND field=%s
312                            """, (changes[0]['author'], changes[0]['time'],
313                                  form_id, field))
314                    else:
315                        # delete current field info
316                        cursor.execute("""
317                            DELETE
318                            FROM    forms_fields
319                            WHERE   id=%s
320                               AND  field=%s
321                            """, (form_id, field))
322        if step == 0:
323            # reset all fields and delete full history
324            for form_id in form_ids:
325                cursor.execute("""
326                    DELETE
327                    FROM    forms_history
328                    WHERE   id=%s
329                    """, (form_id,))
330                cursor.execute("""
331                    DELETE
332                    FROM    forms_fields
333                    WHERE   id=%s
334                    """, (form_id,))
335                # don't delete basic form reference but save the reset
336                # as a form change to prevent creation of a new form_id
337                # for further retention data
338                cursor.execute("""
339                    UPDATE  forms
340                        SET author=%s,
341                            time=%s,
342                            state=%s
343                    WHERE   id=%s
344                    """, (author, int(time.time()), '{}', form_id))
345        db.commit()
346
347    def search_tracforms(self, env, terms, db=None):
348        """Backend method for TracForms ISearchSource implementation."""
349        db = self._get_db(db)
350        cursor = db.cursor()
351        sql, args = search_to_sql(db, ['resource_id', 'subcontext', 'author',
352                                       'state', db.cast('id', 'text')], terms)
353        cursor.execute("""
354            SELECT id,realm,resource_id,subcontext,state,author,time
355            FROM forms
356            WHERE %s
357            """ % sql, args)
358        return cursor.fetchall()
359
360    ##########################################################################
361    # TracForms schemas
362    # Hint: See older versions of this file for the original SQL statements.
363    #   Most of them have been rewritten to imrove compatibility with Trac.
364
365    #def dbschema_2008_06_14_0000(self, cursor):
366    #    """This was a simple test for the schema base class."""
367
368    def db00(self, env, cursor):
369        """Create the major tables."""
370        tables = [
371            Table('tracform_forms', key='id')[
372                Column('tracform_id', auto_increment=True),
373                Column('context'),
374                Column('state'),
375                Column('updater'),
376                Column('updated_on', type='int')],
377            Table('tracform_history')[
378                Column('tracform_id', type='int'),
379                Column('updater'),
380                Column('updated_on', type='int'),
381                Column('old_states')]
382        ]
383        db_connector, _ = DatabaseManager(env)._get_connector()
384        for table in tables:
385            for stmt in db_connector.to_sql(table):
386                cursor.execute(stmt)
387
388    def db01(self, env, cursor):
389        """Create indices for tracform_forms table."""
390        cursor.execute("""
391            CREATE INDEX tracform_forms_context
392                ON tracform_forms(context)
393            """)
394        cursor.execute("""
395            CREATE INDEX tracform_forms_updater
396                ON tracform_forms(updater)
397            """)
398        cursor.execute("""
399            CREATE INDEX tracform_forms_updated_on
400                ON tracform_forms(updated_on)
401            """)
402
403    def db02(self, env, cursor):
404        """This was a modify table, but instead removed the data altogether.
405        """
406
407    def db03(self, env, cursor):
408        """Create indices for tracform_history table."""
409        cursor.execute("""
410            CREATE INDEX tracform_history_tracform_id
411                ON tracform_history(tracform_id)
412            """)
413        # 'DESC' order removed for compatibility with PostgreSQL
414        cursor.execute("""
415            CREATE INDEX tracform_history_updated_on
416                ON tracform_history(updated_on)
417            """)
418        cursor.execute("""
419            CREATE INDEX tracform_history_updater
420                ON tracform_history(updater)
421            """)
422
423    def db04(self, env, cursor):
424        """Recreating updated_on index for tracform_forms to be descending.
425        """
426        # Providing compatibility for PostgreSQL this is now obsolete,
427        # removing misspelled index name creation SQL statement too.
428
429    def db10(self, env, cursor):
430        """Also maintain whether history should be maintained for form."""
431        cursor.execute("""
432            ALTER TABLE tracform_forms
433                ADD keep_history INTEGER
434            """)
435
436    def db11(self, env, cursor):
437        """Make the context a unique index."""
438        if env.config.get('trac', 'database').startswith('mysql'):
439            cursor.execute("""
440                ALTER TABLE tracform_forms DROP INDEX tracform_forms_context
441                """)
442        else:
443            cursor.execute("""
444                DROP INDEX tracform_forms_context
445                """)
446        cursor.execute("""
447            CREATE UNIQUE INDEX tracform_forms_context
448                ON tracform_forms(context)
449            """)
450
451    def db12(self, env, cursor):
452        """Track who changes individual fields."""
453        cursor.execute("""
454            ALTER TABLE tracform_forms
455                ADD track_fields INTEGER
456            """)
457        table = Table('tracform_fields')[
458            Column('tracform_id', type='int'),
459            Column('field'),
460            Column('updater'),
461            Column('updated_on', type='int'),
462            Index(['tracform_id', 'field'], unique=True)
463        ]
464        db_connector, _ = DatabaseManager(env)._get_connector()
465        for stmt in db_connector.to_sql(table):
466            cursor.execute(stmt)
467
468    def db13(self, env, cursor):
469        """Convert state serialization type to be more readable.
470
471        Migrate to slicker named major tables and associated indexes too.
472        """ 
473        table = Table('forms', key='id')[
474            Column('id', auto_increment=True),
475            Column('context'),
476            Column('state'),
477            Column('author'),
478            Column('time', type='int'),
479            Column('keep_history', type='int'),
480            Column('track_fields', type='int'),
481            Index(['context'], unique=True),
482            Index(['author']),
483            Index(['time'])
484        ]
485        db_connector, _ = DatabaseManager(env)._get_connector()
486        for stmt in db_connector.to_sql(table):
487            cursor.execute(stmt)
488
489        forms_columns = ('tracform_id', 'context', 'state', 'updater',
490                         'updated_on', 'keep_history', 'track_fields')
491        forms_columns_new = ('id', 'context', 'state', 'author',
492                             'time', 'keep_history', 'track_fields')
493
494        sql = 'SELECT ' + ', '.join(forms_columns) + ' FROM tracform_forms'
495        cursor.execute(sql)
496        forms = []
497        for row in cursor:
498            row = dict(zip(forms_columns_new, row))
499            forms.append(row)
500
501        # convert current states serialization
502        for form in forms:
503            state_new = _url_to_json(form.get('state'))
504            if state_new == '{}':
505                form['state'] = form.get('state')
506            else:
507                form['state'] = state_new
508
509        for form in forms:
510            fields = form.keys()
511            values = form.values()
512            sql = "INSERT INTO forms (" + ", ".join(fields) + \
513              ") VALUES (" + ", ".join(["%s" for I in xrange(len(fields))]) \
514              + ")"
515            cursor.execute(sql, values)
516
517        cursor.execute("""
518            DROP TABLE tracform_forms
519            """)
520        # migrate history table
521        if env.config.get('trac', 'database').startswith('postgres'):
522            cursor.execute("""
523                CREATE TABLE forms_history
524                    AS SELECT
525                         tracform_id AS id, updated_on AS time,
526                         updater AS author, old_states AS old_state
527                    FROM tracform_history
528                """)
529        else:
530            cursor.execute("""
531                CREATE TABLE forms_history
532                    AS SELECT
533                         tracform_id 'id', updated_on 'time',
534                         updater 'author', old_states 'old_state'
535                    FROM tracform_history
536                """)
537
538        sql = 'SELECT id,time,old_state FROM forms_history'
539        cursor.execute(sql)
540        history = []
541        for row in cursor:
542            row = dict(zip(('id', 'time', 'old_state'), row))
543            history.append(row)
544
545        # convert historic states serialization
546        for row in history:
547            old_state_new = _url_to_json(row.get('old_state'))
548            if old_state_new == '{}':
549                row['old_state'] = row.get('old_state')
550            else:
551                row['old_state'] = old_state_new
552
553        for row in history:
554            sql = "UPDATE forms_history SET old_state=%s " + \
555              "WHERE id=%s AND time=%s"
556            cursor.execute(sql, (row['old_state'], row['id'], row['time']))
557
558        cursor.execute("""
559            CREATE INDEX forms_history_id_idx
560                ON forms_history(id)
561            """)
562        # 'DESC' order removed for compatibility with PostgreSQL
563        cursor.execute("""
564            CREATE INDEX forms_history_time_idx
565                ON forms_history(time)
566            """)
567        cursor.execute("""
568            CREATE INDEX forms_history_author_idx
569                ON forms_history(author)
570            """)
571        cursor.execute("""
572            DROP TABLE tracform_history
573            """)
574        # migrate fields table
575        if env.config.get('trac', 'database').startswith('postgres'):
576            cursor.execute("""
577                CREATE TABLE forms_fields
578                    AS SELECT
579                         tracform_id AS id, field,
580                         updater AS author, updated_on AS time
581                    FROM tracform_fields
582                """)
583        else:
584            cursor.execute("""
585                CREATE TABLE forms_fields
586                    AS SELECT
587                         tracform_id 'id', field,
588                         updater 'author', updated_on 'time'
589                    FROM tracform_fields
590                """)
591        cursor.execute("""
592            CREATE UNIQUE INDEX forms_fields_id_field_idx
593                ON forms_fields(id, field)
594            """)
595        cursor.execute("""
596            DROP TABLE tracform_fields
597            """)
598        # remove old TracForms version entry
599        cursor.execute("""
600            DELETE FROM system WHERE name='TracFormDBComponent:version';
601            """)
602
603    def db14(self, env, cursor):
604        """Split context into proper Trac resource descriptors.""" 
605        cursor.execute("""
606            CREATE TABLE forms_old
607                AS SELECT *
608                FROM forms
609            """)
610        cursor.execute("""
611            DROP TABLE forms
612            """)
613        table = Table('forms', key='id')[
614            Column('id', auto_increment=True),
615            Column('realm'),
616            Column('resource_id'),
617            Column('subcontext'),
618            Column('state'),
619            Column('author'),
620            Column('time', type='int'),
621            Column('keep_history', type='int'),
622            Column('track_fields', type='int'),
623            Index(['realm', 'resource_id', 'subcontext'], unique=True),
624            Index(['author']),
625            Index(['time'])
626        ]
627        db_connector, _ = DatabaseManager(env)._get_connector()
628        for stmt in db_connector.to_sql(table):
629            cursor.execute(stmt)
630
631        forms_columns = ('id', 'context', 'state', 'author',
632                         'time', 'keep_history', 'track_fields')
633
634        sql = 'SELECT ' + ', '.join(forms_columns) + ' FROM forms_old'
635        cursor.execute(sql)
636        forms = []
637        for row in cursor:
638            row = dict(zip(forms_columns, row))
639            # extract realm, resource_id and subcontext from context
640            row['realm'], row['resource_id'], row['subcontext'] = \
641                _context_to_resource(self.env, row.pop('context'))
642            forms.append(row)
643
644        for form in forms:
645            fields = form.keys()
646            values = form.values()
647            sql = "INSERT INTO forms (" + ", ".join(fields) + \
648              ") VALUES (" + ", ".join(["%s" for I in xrange(len(fields))]) \
649              + ")"
650            cursor.execute(sql, values)
651
652        cursor.execute("""
653            DROP TABLE forms_old
654            """)
655
656
657def _url_to_json(state_url):
658    """Convert urlencoded state serial to JSON state serial."""
659    state = parse_qs(state_url)
660    for name, value in state.iteritems():
661        if isinstance(value, (list, tuple)):
662            for item in value:
663                state[name] = xml_unescape(item)
664        else:
665            state[name] = xml_unescape(value)
666    return json.dumps(state, separators=(',', ':'))
667
668def _context_to_resource(env, context):
669    """Find parent realm and resource_id and optional TracForms subcontext.
670
671    Some guesswork is knowingly involved here to finally overcome previous
672    potentially ambiguous contexts by distinct resource parameters.
673    """
674    realm, resource_path = resource_from_page(env, context)
675    subcontext = None
676    if resource_path is not None:
677        # ambiguous: ':' could be part of resource_id or subcontext or
678        # the start of subcontext
679        segments = re.split(':', resource_path)
680        id = ''
681        resource_id = None
682        while len(segments) > 0:
683            id += segments.pop(0)
684            # guess: shortest valid resource_id is parent,
685            # the rest is a TracForms subcontext
686            if resource_exists(env, Resource(realm, id)):
687                resource_id = id
688                subcontext = ':'.join(segments)
689                break
690            id += ':'
691        # valid resource_id in context NOT FOUND
692        if resource_id is None:
693            resource_id = resource_path
694    else:
695        # realm in context NOT FOUND
696        resource_id = context
697        realm = ''
698    return realm, resource_id, subcontext
699
700
701if __name__ == '__main__':
702    from trac.test import EnvironmentStub
703    env = EnvironmentStub()
704    db = FormDBComponent(env)
705    db.upgrade_environment(None)
706    updated_on_1 = db.save_tracform('/', 'hello world', 'me')[0][4]
707    assert db.get_tracform_state('/') == 'hello world'
708    updated_on_2 = \
709        db.save_tracform('/', 'ack oop', 'you', updated_on_1)[0][4]
710    assert db.get_tracform_state('/') == 'ack oop'
711    assert tuple(db.get_tracform_history('/')) == (
712        ('me', updated_on_1, 'hello world'),
713        )
714
Note: See TracBrowser for help on using the repository browser.