Add hooks to update projects modification_date upon ticket/version/subproject events

This is related to #2723082, as it allows projects to be properly sorted based on their modification_date.

The tests cover:

  • entity addition: an entity (ticket, version, subproject) is added to each project in a random order, we test that the list of projects ordered by modification_date matches the order of entity addition;
  • relation updates: relation between project and entities are changed, we test that the modification_date of the modified project is greater than its initial one;
  • projects deletion.
authorDenis Laxalde <denis.laxalde@logilab.fr>
changesetc22660aa69cc
branchdefault
phasepublic
hiddenno
parent revision#29664a3a95a6 [view] add ticket type to icon (closes #2741644)
child revision#ffd655535c2d Improve the projects list view and add an icon attribute to Project, #da232a989989 Set Version publication_date on publish if it's not already set, #e5a8cca8545c Set Version publication_date on publish if it's not already set, #ab5ba28016b9 Add a publish() method on Version, #a7415e91603d Fix Version.start_date
files modified by this revision
hooks.py
test/unittest_tracker.py
# HG changeset patch
# User Denis Laxalde <denis.laxalde@logilab.fr>
# Date 1362746058 -3600
# Fri Mar 08 13:34:18 2013 +0100
# Node ID c22660aa69cc18e5035bbc7f7144b012e5d22a72
# Parent 29664a3a95a61c017482d44f512466ef88fb1b75
Add hooks to update projects modification_date upon ticket/version/subproject events

This is related to #2723082, as it allows projects to be properly sorted based
on their modification_date.

The tests cover:

- entity addition: an entity (ticket, version, subproject) is added to each
project in a random order, we test that the list of projects ordered by
modification_date matches the order of entity addition;
- relation updates: relation between project and entities are changed, we test
that the modification_date of the modified project is greater than its
initial one;
- projects deletion.

diff --git a/hooks.py b/hooks.py
@@ -4,10 +4,12 @@
1  :copyright: 2006-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
3  """
4  __docformat__ = "restructuredtext en"
5 
6 +from datetime import datetime
7 +
8  from cubicweb import ValidationError
9  from cubicweb.utils import transitive_closure_of
10  from cubicweb.schema import META_RTYPES
11  from cubicweb.selectors import is_instance
12  from cubicweb.server import hook
@@ -15,10 +17,70 @@
13 
14  from cubes.localperms import hooks as localperms
15 
16  # automatization hooks #########################################################
17 
18 +class RelationChange(hook.Hook):
19 +    """Update modification_date upon relation change"""
20 +    __regid__ = 'tracker.relation_change_hook'
21 +    __select__ = hook.Hook.__select__ & hook.match_rtype('concerns', 'done_in',
22 +                                                         'appeared_in', 'version_of',
23 +                                                         'subproject_of')
24 +    events = ('after_add_relation', 'before_delete_relation')
25 +
26 +    def __call__(self):
27 +        if self.rtype in ('concerns', 'version_of', 'subproject_of'):
28 +            projecteid = self.eidto
29 +        else:
30 +            projecteid = self._cw.execute('Project P WHERE X concerns P, X eid %(x)s',
31 +                                          {'x': self.eidfrom}).rows[0][0]
32 +        ProjectModificationDateUpdate.get_instance(self._cw).add_data(projecteid)
33 +
34 +class TicketVersionChange(hook.Hook):
35 +    """Update modification_date upon ticket/version change"""
36 +    __regid__ = 'tracker.ticket_or_version_change_hook'
37 +    __select__ = hook.Hook.__select__ & is_instance('Ticket', 'Version')
38 +    events = ('after_add_entity', 'after_update_entity')
39 +
40 +    def __call__(self):
41 +        ProjectModificationDateUpdateFromEntity.get_instance(self._cw).add_data(self.entity)
42 +
43 +class TicketVersionDelete(hook.Hook):
44 +    """Update modification_date upon ticket/version deletion"""
45 +    __regid__ = 'tracker.ticket_or_version_delete_hook'
46 +    __select__ = hook.Hook.__select__ & is_instance('Ticket', 'Version')
47 +    events = ('before_delete_entity', )
48 +
49 +    def __call__(self):
50 +        try:
51 +            project = self.entity.project
52 +        except IndexError:
53 +            # the entity is not anymore attached to the project, e.g. in case
54 +            # the project is being deleted
55 +            pass
56 +        else:
57 +            ProjectModificationDateUpdate.get_instance(self._cw).add_data(project.eid)
58 +
59 +class ProjectModificationDateUpdate(hook.DataOperationMixIn, hook.Operation):
60 +    """Base operation to update modification_date, works when the project is
61 +    already attached to the entity."""
62 +    def get_projecteid(self, data):
63 +        return data
64 +
65 +    def precommit_event(self):
66 +        now = datetime.now()
67 +        for data in self.get_data():
68 +            projecteid = self.get_projecteid(data)
69 +            self.session.execute('SET P modification_date %(d)s WHERE P eid %(p)s',
70 +                                 {'p': projecteid, 'd': now})
71 +
72 +class ProjectModificationDateUpdateFromEntity(ProjectModificationDateUpdate):
73 +    """Update modification_date from changes in an entity, when projects is
74 +    not yet attached to entity."""
75 +    def get_projecteid(self, entity):
76 +        return entity.project.eid
77 +
78  class VersionStatusChangeHook(hook.Hook):
79      """when a ticket is done, automatically set its version'state to 'dev' if
80        necessary
81      """
82      __regid__ = 'version_status_change_hook'
diff --git a/test/unittest_tracker.py b/test/unittest_tracker.py
@@ -1,9 +1,10 @@
83  """Tracker unit tests"""
84  from __future__ import with_statement
85 
86  from datetime import datetime, timedelta
87 +from random import sample
88 
89  from logilab.common.testlib import unittest_main, mock_object
90 
91  from cubicweb import Unauthorized, ValidationError
92  from cubicweb.utils import transitive_closure_of
@@ -586,7 +587,83 @@
93          self.assertEqual(len(self.lgc.reverse_concerns), 1)
94          self.assertEqual(self.execute('Any COUNT(T) WHERE T done_in V, V in_state S, S name "%s"'
95                                         % self.versions_wf_map['inprogress']).rows[0][0],
96                            3)
97 
98 +class ProjectModificationDate(TrackerBaseTC):
99 +    """Tests hooks and operations that update modification_date on projects"""
100 +    def setup_database(self):
101 +        req = self.request()
102 +        # create a few projects
103 +        self.projects = [req.create_entity('Project', name=name, description=desc)
104 +                         for name, desc in zip((u'cubicweb', u'cube1', u'cube2'),
105 +                                               (u'framework', u'cube', u'another cube'))]
106 +        # random sample of available projects
107 +        self.rand_projects = sample(self.projects, len(self.projects))
108 +
109 +    def setup_entities(self):
110 +        """Add entities to available projects"""
111 +        entities = [('Ticket', u'bug'), ('Version', u'0.1'), ('Project', u'kid')]
112 +        req = self.request()
113 +        for p, o in zip(self.rand_projects, entities):
114 +            if o[0] == 'Ticket':
115 +                t = req.create_entity('Ticket', title=o[1])
116 +                self.execute('SET T concerns P WHERE T eid %(t)s, P name %(p)s',
117 +                             {'t' : t.eid, 'p' : p.name})
118 +            elif o[0] == 'Version':
119 +                v = req.create_entity('Version', num=o[1])
120 +                self.execute('SET V version_of P WHERE V eid %(v)s, P eid %(p)s',
121 +                             {'v' : v.eid, 'p' : p.eid})
122 +            elif o[0] == 'Project':
123 +                c = req.create_entity('Project', name=o[1])
124 +                self.execute('SET C subproject_of P WHERE C name %(c)s, P name %(p)s',
125 +                             {'c': c.name, 'p': p.name})
126 +            else:
127 +                continue
128 +            self.commit()
129 +
130 +    def get_ordered_projects(self):
131 +        """Returns the list of projects eid ordered by modification_date, within the
132 +        initial projects set."""
133 +        return [p[0] for p in self.execute('Any P ORDERBY MD WHERE P is Project, P modification_date MD')
134 +                if p[0] in [bp.eid for bp in self.projects]]
135 +
136 +    def get_modification_date(self, projecteid):
137 +        """Return the modification_date of a project given its eid"""
138 +        return self.execute('Any MD WHERE P eid %(p)s, P modification_date MD',
139 +                            {'p': projecteid})[0][0]
140 +
141 +    def test_add_entities(self):
142 +        self.setup_entities()
143 +        ordered_projs = self.get_ordered_projects()
144 +        self.assertEqual(ordered_projs, [p.eid for p in self.rand_projects])
145 +
146 +    def test_update_relations(self):
147 +        self.setup_entities()
148 +        projects_md = dict((p.eid, self.get_modification_date(p.eid))
149 +                           for p in self.projects)
150 +        for enttype, relation in zip(['Ticket', 'Version', 'Project'],
151 +                                     ['concerns', 'version_of', 'subproject_of']):
152 +            entity, oldproject = self.execute('Any E, P WHERE E is %s, E %s P'
153 +                                              % (enttype, relation))[0]
154 +            newproject = sample(projects_md.keys(), 1)[0]
155 +            self.execute('SET E %s P WHERE E eid %%(e)s, P eid %%(p)s' % relation,
156 +                         {'e': entity, 'p': newproject})
157 +            self.commit()
158 +            self.assertGreater(self.get_modification_date(oldproject),
159 +                               projects_md[oldproject])
160 +            self.assertGreater(self.get_modification_date(newproject),
161 +                               projects_md[newproject])
162 +
163 +    def test_delete_projects(self):
164 +        self.setup_entities()
165 +        for p in self.projects:
166 +            try:
167 +                self.execute('DELETE Project P WHERE P eid %(p)s' % {'p': p.eid})
168 +                self.commit()
169 +            except:
170 +                print 'Fail to delete project', p.name
171 +                raise
172 +
173 +
174  if __name__ == '__main__':
175      unittest_main()