source: tagsplugin/tags/0.8/tractags/model.py

Last change on this file was 13854, checked in by Steffen Hoffmann, 9 years ago

TagsPlugin: Prepare tag change records for timeline view, refs #11661.

This is the minimal solution proving only changes recorded by TagsPlugin.

Since [13428] tag changes are saved, but only for wiki pages by default.
So changes recorded with tagged resources like tickets are left out here.
Hint: Use realm's native timeline event providers (i. e. ticket_show_details
option in trac.ini section [timeline] for tickets) to get them.

  • Property svn:eol-style set to native
File size: 7.1 KB
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright (C) 2014 Jun Omae <jun66j5@gmail.com>
5# Copyright (C) 2012,2013 Steffen Hoffmann <hoff.st@web.de>
6#
7# This software is licensed as described in the file COPYING, which
8# you should have received as part of this distribution.
9
10from datetime import datetime
11
12from trac.resource import Resource
13from trac.util.compat import groupby, set
14from trac.util.datefmt import utc
15from trac.util.text import to_unicode
16
17from tractags.compat import to_datetime, to_utimestamp
18from tractags.util import split_into_tags
19
20
21# Public functions (not yet)
22
23
24# Utility functions
25
26def delete_tags(env, resource, tags=None, db=None):
27    """Delete tags and tag changes for a Trac resource."""
28    do_commit = db is None or False
29    db = _get_db(env, db)
30    cursor = db.cursor()
31    args = [resource.realm, to_unicode(resource.id)]
32    sql = ''
33    if tags:
34        args += list(tags)
35        sql += " AND tags.tag IN (%s)" % ','.join(['%s' for tag in tags])
36    cursor.execute("""
37        DELETE FROM tags
38         WHERE tagspace=%%s
39           AND name=%%s%s
40    """ % sql, args)
41    if do_commit:
42        # Call outside of another db transaction means resource destruction,
43        # so purge change records too.
44        cursor.execute("""
45            DELETE FROM tags_change
46             WHERE tagspace=%s
47               AND name=%s
48        """, (resource.realm, to_unicode(resource.id)))
49        db.commit()
50
51def tag_changes(env, resource, start=None, stop=None):
52    """Return tag history for one or all tagged Trac resources."""
53    db = _get_db(env)
54    cursor = db.cursor()
55    if resource:
56        # Resource changelog events query.
57        cursor.execute("""
58            SELECT time,author,oldtags,newtags
59              FROM tags_change
60             WHERE tagspace=%s
61               AND name=%s
62             ORDER BY time DESC
63        """, (resource.realm, to_unicode(resource.id)))
64        return [(to_datetime(row[0]), row[1], row[2], row[3])
65                for row in cursor]
66    # Timeline events query.
67    cursor.execute("""
68        SELECT time,author,tagspace,name,oldtags,newtags
69          FROM tags_change
70         WHERE time>%s
71           AND time<%s
72    """, (to_utimestamp(start), to_utimestamp(stop)))
73    return [(to_datetime(row[0]), row[1], row[2], row[3], row[4], row[5])
74            for row in cursor]
75
76def tag_frequency(env, realm, filter=None, db=None):
77    """Return tags and numbers of their occurrence."""
78    if filter:
79        sql = ''.join([" AND %s" % f for f in filter])
80    db = _get_db(env, db)
81    cursor = db.cursor()
82    cursor.execute("""
83        SELECT tag,count(tag)
84          FROM tags
85         WHERE tagspace=%%s%s
86         GROUP BY tag
87    """ % (filter and sql or ''), (realm,))
88    for row in cursor:
89        yield (row[0], row[1])
90
91def tag_resource(env, resource, old_id=None, author='anonymous', tags=[],
92                 log=False, when=None):
93    """Save tags and tag changes for a Trac resource.
94
95    This function combines delete, reparent and set actions now, but it could
96    possibly be still a bit more efficient.
97    """
98    db = _get_db(env)
99    cursor = db.cursor()
100    if when is None:
101        when = datetime.now(utc)
102    if isinstance(when, datetime):
103        when = to_utimestamp(when)
104
105    if old_id:
106        cursor.execute("""
107            UPDATE tags
108               SET name=%s
109             WHERE tagspace=%s
110               AND name=%s
111        """, (to_unicode(resource.id), resource.realm, to_unicode(old_id)))
112        cursor.execute("""
113            UPDATE tags_change
114               SET name=%s
115             WHERE tagspace=%s
116               AND name=%s
117        """, (to_unicode(resource.id), resource.realm, to_unicode(old_id)))
118    else:
119        # Calculate effective tag changes.
120        old_tags = set(resource_tags(env, resource, db=db))
121        tags = set(tags)
122        remove = old_tags - tags
123        if remove:
124            if tags:
125                delete_tags(env, resource, remove, db=db)
126            else:
127                # Delete all resource's tags - simplified transaction.
128                delete_tags(env, resource, db=db)
129        add = tags - old_tags
130        if add:
131            cursor.executemany("""
132                INSERT INTO tags
133                       (tagspace, name, tag)
134                VALUES (%s,%s,%s)
135            """, [(resource.realm, to_unicode(resource.id), tag)
136                  for tag in add])
137        if log:
138            cursor.execute("""
139                INSERT INTO tags_change
140                       (tagspace, name, time, author, oldtags, newtags)
141                VALUES (%s,%s,%s,%s,%s,%s)
142            """, (resource.realm, to_unicode(resource.id), when, author,
143                  u' '.join(sorted(map(to_unicode, old_tags))),
144                  u' '.join(sorted(map(to_unicode, tags))),))
145    db.commit()
146
147def tagged_resources(env, perm_check, perm, realm, tags=None, filter=None,
148                     db=None):
149    """Return Trac resources including their associated tags.
150
151    This is currently known to be a major performance hog.
152    """
153    db = _get_db(env, db)
154    cursor = db.cursor()
155
156    args = [realm]
157    sql = """
158        SELECT DISTINCT name
159          FROM tags
160         WHERE tagspace=%s"""
161    if filter:
162        sql += ''.join([" AND %s" % f for f in filter])
163    if tags:
164        sql += " AND tags.tag IN (%s)" % ','.join(['%s' for tag in tags])
165        args += tags
166    sql += " ORDER by name"
167    cursor.execute(sql, args)
168
169    # Inlined permission check for efficiency.
170    resources = {}
171    for name, in cursor:
172        resource = Resource(realm, name)
173        if perm_check(perm(resource), 'view'):
174            resources[resource.id] = resource
175    if not resources:
176        return
177
178    args = [realm] + resources.keys()
179    # DEVEL: Is this going to be excruciatingly slow?
180    #        The explicite resource ID list might even grow beyond some limit.
181    sql = """
182        SELECT DISTINCT name, tag
183          FROM tags
184         WHERE tagspace=%%s
185           AND name IN (%s)
186         ORDER BY name
187    """ % ', '.join(['%s' for resource in resources])
188    cursor.execute(sql, args)
189
190    for name, tags in groupby(cursor, lambda row: row[0]):
191        resource = resources[name]
192        yield resource, set([tag[1] for tag in tags])
193
194def resource_tags(env, resource, db=None, when=None):
195    """Return all tags for a Trac resource by realm and ID."""
196    db = _get_db(env, db)
197    cursor = db.cursor()
198    id = to_unicode(resource.id)
199    if when is None:
200        cursor.execute("""
201            SELECT tag
202              FROM tags
203             WHERE tagspace=%s
204               AND name=%s
205        """, (resource.realm, id))
206        for row in cursor:
207            yield row[0]
208    else:
209        if isinstance(when, datetime):
210            when = to_utimestamp(when)
211        cursor.execute("SELECT newtags FROM tags_change "
212                       "WHERE tagspace=%s AND name=%s AND time<=%s "
213                       "ORDER BY time DESC LIMIT 1",
214                       (resource.realm, id, when))
215        row = cursor.fetchone()
216        if row and row[0]:
217            for tag in split_into_tags(row[0]):
218                yield tag
219
220# Internal functions
221
222def _get_db(env, db=None):
223    return db or env.get_db_cnx()
Note: See TracBrowser for help on using the repository browser.