Edgewall Software

NewWorkflow: patch-newworkflow-r1098.diff

File patch-newworkflow-r1098.diff , 90.3 KB (added by pkou <pkou at ua.fm>, 4 years ago)

Patch synchronized with Trac 0.8

Line 
1Index: htdocs/css/timeline.css
2===================================================================
3--- htdocs/css/timeline.css     (revision 1098)
4+++ htdocs/css/timeline.css     (working copy)
5@@ -35,6 +35,8 @@
6 /* Apply icon background-image twice to avoid hover-flicker in IE/Win */
7 dt.changeset, dt.changeset a { background-image: url(../changeset.png) !important }
8 dt.newticket, dt.newticket a { background-image: url(../newticket.png) !important }
9+dt.resolvedticket, dt.resolvedticket a { background-image: url(../resolvedticket.png) !important }
10+dt.reopenedticket, dt.reopenedticket a { background-image: url(../reopenedticket.png) !important }
11 dt.closedticket, dt.closedticket a { background-image: url(../closedticket.png) !important }
12 dt.wiki, dt.wiki a { background-image: url(../wiki.png) !important }
13 dt.milestone, dt.milestone a { background-image: url(../milestone.png) !important }
14Index: wiki-default/TracIni
15===================================================================
16--- wiki-default/TracIni        (revision 1098)
17+++ wiki-default/TracIni        (working copy)
18@@ -27,6 +27,7 @@
19 See also: TracLogging
20 
21 == [ticket] ==
22+|| workflow || Ticket workflow class.  If not specified, it is ''trac.workflows.SimpleWorkflow'' ||
23 || default_version   || Default version for newly created tickets ||
24 || default_severity  || Default severity for newly created tickets ||
25 || default_priority  || Default priority for newly created tickets ||
26@@ -66,4 +67,4 @@
27 
28 [[BR]]
29 ----
30-See also: TracGuide, TracAdmin
31\ No newline at end of file
32+See also: TracGuide, TracAdmin
33Index: wiki-default/TracAdmin
34===================================================================
35--- wiki-default/TracAdmin      (revision 1098)
36+++ wiki-default/TracAdmin      (working copy)
37@@ -28,10 +28,10 @@
38 permission add <user> <action> [action] [...]     -- Add a new permission rule                             
39 permission remove <user> <action> [action] [...]  -- Remove permission rule                               
40 component list                                    -- Show available components                             
41-component add <name> <owner>                      -- Add a new component                                   
42+component add <name> <owner> [<qaowner>]          -- Add a new component                                   
43 component rename <name> <newname>                 -- Rename a component                                   
44 component remove <name>                           -- Remove/uninstall component                           
45-component chown <name> <owner>                    -- Change component ownership                           
46+component chown <name> <owner> [<qaowner>]        -- Change component ownership                           
47 priority list                                     -- Show possible ticket priorities                       
48 priority add <value>                              -- Add a priority value option                           
49 priority change <value> <newvalue>                -- Change a priority value                               
50@@ -46,10 +46,11 @@
51 version time <name> <time>                        -- Set version date (Format: "Jun 3, 2003")             
52 version remove <name>                             -- Remove version                                       
53 milestone list                                    -- Show milestones                                       
54-milestone add <name> [time]                       -- Add milestone                                         
55+milestone add <name> [<owner> [time]]             -- Add milestone                                         
56 milestone rename <name> <newname>                 -- Rename milestone                                     
57 milestone time <name> <time>                      -- Set milestone date (Format: "Jun 3, 2003")           
58 milestone remove <name>                           -- Remove milestone                                     
59+milestone chown <name> <owner>                    -- Change milestone ownership
60 }}}
61 
62 == Interactive Mode ==
63@@ -63,4 +64,4 @@
64 
65 
66 ----
67-See Also: TracGuide, TracBackup, TracPermissions. TracEnvironment, TracIni
68\ No newline at end of file
69+See Also: TracGuide, TracBackup, TracPermissions. TracEnvironment, TracIni
70Index: setup.py
71===================================================================
72--- setup.py    (revision 1098)
73+++ setup.py    (working copy)
74@@ -198,7 +198,8 @@
75       author_email="info@edgewall.com",
76       license=LICENSE,
77       url=URL,
78-      packages=['trac', 'trac.upgrades', 'trac.wikimacros', 'trac.mimeviewers'],
79+      packages=['trac', 'trac.upgrades', 'trac.wikimacros', 'trac.mimeviewers',
80+                'trac.workflows'],
81       data_files=[(_p('share/trac/templates'), glob('templates/*')),
82                   (_p('share/trac/htdocs'), glob(_p('htdocs/*.*')) + [_p('htdocs/README')]),
83                   (_p('share/trac/htdocs/css'), glob(_p('htdocs/css/*'))),
84Index: scripts/trac-admin
85===================================================================
86--- scripts/trac-admin  (revision 1098)
87+++ scripts/trac-admin  (working copy)
88@@ -290,10 +290,10 @@
89     
90 #    ## Component
91     _help_component = [('component list', 'Show available components'),
92-                       ('component add <name> <owner>', 'Add a new component'),
93+                       ('component add <name> <owner> [<qaowner>]', 'Add a new component'),
94                        ('component rename <name> <newname>', 'Rename a component'),
95                        ('component remove <name>', 'Remove/uninstall component'),
96-                       ('component chown <name> <owner>', 'Change component ownership')]
97+                       ('component chown <name> <owner> [<qaowner>]', 'Change component ownership')]
98 
99     def complete_component (self, text, line, begidx, endidx):
100         if begidx in [16,17]:
101@@ -309,10 +309,14 @@
102         try:
103             if arg[0]  == 'list':
104                 self._do_component_list()
105-            elif arg[0] == 'add' and len(arg)==3:
106+            elif arg[0] == 'add' and len(arg) in [3,4]:
107                 name = arg[1]
108                 owner = arg[2]
109-                self._do_component_add(name, owner)
110+                if len(arg) == 4:
111+                    qaowner = arg[3]
112+                else:
113+                    qaowner = owner
114+                self._do_component_add(name, owner, qaowner)
115             elif arg[0] == 'rename' and len(arg)==3:
116                 name = arg[1]
117                 newname = arg[2]
118@@ -320,22 +324,26 @@
119             elif arg[0] == 'remove'  and len(arg)==2:
120                 name = arg[1]
121                 self._do_component_remove(name)
122-            elif arg[0] == 'chown' and len(arg)==3:
123+            elif arg[0] == 'chown' and len(arg) in [3,4]:
124                 name = arg[1]
125                 owner = arg[2]
126-                self._do_component_set_owner(name, owner)
127+                if len(arg) == 4:
128+                    qaowner = arg[3]
129+                else:
130+                    qaowner = owner
131+                self._do_component_set_owner(name, owner, qaowner)
132             else:   
133                 self.do_help ('component')
134         except Exception, e:
135             print 'Component %s failed:' % arg[0], e
136 
137     def _do_component_list(self):
138-        data = self.db_execsql('SELECT name, owner FROM component')
139-        self.print_listing(['Name', 'Owner'], data)
140+        data = self.db_execsql('SELECT name, owner, qaowner FROM component')
141+        self.print_listing(['Name', 'Owner', 'QA Owner'], data)
142 
143-    def _do_component_add(self, name, owner):
144-        data = self.db_execsql("INSERT INTO component VALUES('%s', '%s')"
145-                               % (name, owner))
146+    def _do_component_add(self, name, owner, qaowner):
147+        data = self.db_execsql("INSERT INTO component VALUES('%s', '%s', '%s')"
148+                               % (name,owner,qaowner))
149 
150     def _do_component_rename(self, name, newname):
151         cnx = self.db_open()
152@@ -360,15 +368,15 @@
153         data = self.db_execsql("DELETE FROM component WHERE name='%s'"
154                                % (name))
155 
156-    def _do_component_set_owner(self, name, owner):
157+    def _do_component_set_owner(self, name, owner, qaowner):
158         cnx = self.db_open()
159         cursor = cnx.cursor ()
160         cursor.execute('SELECT name FROM component WHERE name=%s', name)
161         data = cursor.fetchone()
162         if not data:
163             raise Exception("No such component '%s'" % name)
164-        data = self.db_execsql("UPDATE component SET owner='%s' WHERE name='%s'"
165-                               % (owner,name))
166+        data = self.db_execsql("UPDATE component SET owner='%s', qaowner='%s' WHERE name='%s'"
167+                               % (owner,qaowner,name))
168 
169 
170     ## Permission
171@@ -795,9 +803,10 @@
172 
173     ## Milestone
174     _help_milestone = [('milestone list', 'Show milestones'),
175-                       ('milestone add <name> [time]', 'Add milestone'),
176+                       ('milestone add <name> [<owner> [time]]', 'Add milestone'),
177                        ('milestone rename <name> <newname>',
178                         'Rename milestone'),
179+                       ('milestone chown <name> <newowner>', 'Change milestone owner'),
180                        ('milestone time <name> <time>', 'Set milestone date (Format: "Jun 3, 2003")'),
181                        ('milestone remove <name>', 'Remove milestone')]
182 
183@@ -805,14 +814,54 @@
184 
185         if begidx in [15,17]:
186             comp = self.get_milestone_list ()
187+        elif begidx > 15 and line.startswith('milestone chown '):
188+            comp = self.get_user_list()
189         elif begidx < 15:
190-            comp = ['list','add','rename','time','remove']
191+            comp = ['list','add','rename','chown','time','remove']
192         return self.word_complete(text, comp)
193 
194     def do_milestone(self, line):
195-        self._do_mile_ver('milestone', line)
196+        type = 'milestone'
197+        arg = self.arg_tokenize(line)
198+        try:
199+            if arg[0]  == 'list':
200+                self._do_milestone_list()
201+            elif arg[0] == 'add' and len(arg) in [2,3,4]:
202+                name = arg[1]
203+                self._do_mile_ver_add(type, name)
204+                if len(arg) >= 3:
205+                    owner = arg[2]
206+                    self._do_mile_ver_chown(type, name, owner)
207+                if len(arg) >= 4:
208+                    time = arg[3]
209+                    self._do_mile_ver_time(type, name, time)
210+            elif arg[0] == 'rename' and len(arg)==3:
211+                name = arg[1]
212+                newname = arg[2]
213+                self._do_mile_ver_rename(type, name, newname)
214+            elif arg[0] == 'chown' and len(arg)==3:
215+                name = arg[1]
216+                owner = arg[2]
217+                self._do_mile_ver_chown(type, name, owner)
218+            elif arg[0] == 'time' and len(arg)==3:
219+                name = arg[1]
220+                time = arg[2]
221+                self._do_mile_ver_time(type, name, time)
222+            elif arg[0] == 'remove' and len(arg)==2:
223+                name = arg[1]
224+                self._do_mile_ver_remove(type, name)
225+            else:
226+                self.do_help (type)
227+        except Exception, e:
228+            print 'Command %s failed:' % arg[0], e
229 
230+    def _do_milestone_list(self):
231+        data = self.db_execsql("SELECT name,owner,time FROM milestone ORDER BY time,name")
232+        data = map(lambda x: (x[0], x[1], x[2] and time.strftime('%c', time.localtime(x[2]))), data)
233+        #print data
234+        self.print_listing(['Name', 'Owner', 'Time'], data)
235 
236+
237     ## Version
238     _help_version = [('version list', 'Show versions'),
239                        ('version add <name> [time]', 'Add version'),
240@@ -832,7 +881,7 @@
241     def do_version(self, line):
242         self._do_mile_ver('version', line)
243 
244-    # Milestone and Version are identical,  methods
245+    # Milestone and Version are almost identical,  methods
246 
247     def _do_mile_ver(self, type, line):
248         arg = self.arg_tokenize(line)
249@@ -923,6 +972,10 @@
250         else:
251             print >> sys.stderr, 'Unknown time format'
252 
253+    def _do_mile_ver_chown(self, type, name, owner):
254+        data = self.db_execsql("UPDATE %s SET owner='%s' WHERE name='%s'"
255+                               % (type, owner, name));
256+
257     _help_upgrade = [('upgrade', 'Upgrade database to current version.')]
258     def do_upgrade(self, line):
259         arg = self.arg_tokenize(line)
260Index: trac/db_default.py
261===================================================================
262--- trac/db_default.py  (revision 1098)
263+++ trac/db_default.py  (working copy)
264@@ -21,7 +21,7 @@
265 
266 
267 # Database version identifier. Used for automatic upgrades.
268-db_version = 7
269+db_version = 8
270 
271 def __mkreports(reps):
272     """Utility function used to create report data in same syntax as the
273@@ -125,12 +125,14 @@
274 );
275 CREATE TABLE component (
276          name            text PRIMARY KEY,
277-         owner           text
278+         owner           text,
279+         qaowner         text
280 );
281 CREATE TABLE milestone (
282          id              integer PRIMARY KEY,
283          name            text,
284          time            integer,
285+         owner           text,
286          descr           text,
287          UNIQUE(name)
288 );
289@@ -209,8 +211,8 @@
290 """,
291 """
292 SELECT p.value AS __color__,
293-   version AS __group__,
294-   id AS ticket, summary, component, version, severity,
295+   (CASE WHEN IFNULL(version, '') = '' THEN 'Not Specified' ELSE 'Version ' || version END) AS __group__,
296+   id AS ticket, summary, component, milestone, severity,
297    (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner,
298    time AS created,
299    changetime AS _changetime, description AS _description,
300@@ -218,10 +220,10 @@
301   FROM ticket t, enum p
302   WHERE status IN ('new', 'assigned', 'reopened')
303 AND p.name = t.priority AND p.type = 'priority'
304-  ORDER BY (version IS NULL),version, p.value, severity, time
305+  ORDER BY (IFNULL(version, '') = '') DESC,version, p.value, severity, time
306 """),
307 #----------------------------------------------------------------------------
308-('All Tickets by Milestone',
309+('Active Tickets by Milestone',
310 """
311 This report shows how to color results by priority,
312 while grouping results by milestone.
313@@ -231,7 +233,7 @@
314 """,
315 """
316 SELECT p.value AS __color__,
317-   milestone||' Release' AS __group__,
318+   (CASE WHEN IFNULL(milestone, '') = '' THEN 'Not Assigned' ELSE milestone||' Release' END) AS __group__,
319    id AS ticket, summary, component, version, severity,
320    (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner,
321    time AS created,
322@@ -240,7 +242,7 @@
323   FROM ticket t, enum p
324   WHERE status IN ('new', 'assigned', 'reopened')
325 AND p.name = t.priority AND p.type = 'priority'
326-  ORDER BY (milestone IS NULL),milestone, p.value, severity, time
327+  ORDER BY (IFNULL(milestone, '') = '') DESC,milestone, p.value, severity, time
328 """),
329 #----------------------------------------------------------------------------
330 ('Assigned, Active Tickets by Owner',
331@@ -248,16 +250,15 @@
332 List assigned tickets, group by ticket owner, sorted by priority.
333 """,
334 """
335-
336 SELECT p.value AS __color__,
337-   owner AS __group__,
338-   id AS ticket, summary, component, milestone, severity, time AS created,
339+   (CASE WHEN IFNULL(owner, '') = '' THEN 'Not Assigned' ELSE owner END) AS __group__,
340+   id AS ticket, summary, component, version, milestone, severity, time AS created,
341    changetime AS _changetime, description AS _description,
342    reporter AS _reporter
343   FROM ticket t,enum p
344   WHERE status = 'assigned'
345 AND p.name=t.priority AND p.type='priority'
346-  ORDER BY owner, p.value, severity, time
347+  ORDER BY (IFNULL(owner, '') = '') DESC, owner, p.value, severity, time
348 """),
349 #----------------------------------------------------------------------------
350 ('Assigned, Active Tickets by Owner (Full Description)',
351@@ -268,7 +269,7 @@
352 """
353 SELECT p.value AS __color__,
354    owner AS __group__,
355-   id AS ticket, summary, component, milestone, severity, time AS created,
356+   id AS ticket, summary, component, version, milestone, severity, time AS created,
357    description AS _description_,
358    changetime AS _changetime, reporter AS _reporter
359   FROM ticket t, enum p
360@@ -283,7 +284,7 @@
361 """,
362 """
363 SELECT p.value AS __color__,
364-   t.milestone AS __group__,
365+   (CASE WHEN IFNULL(t.milestone, '') = '' THEN 'Not Assigned' ELSE t.milestone || ' Release' END) AS __group__,
366    (CASE status
367       WHEN 'closed' THEN 'color: #777; background: #ddd; border-color: #ccc;'
368       ELSE
369@@ -295,7 +296,7 @@
370    time AS _time,reporter AS _reporter
371   FROM ticket t,enum p
372   WHERE p.name=t.priority AND p.type='priority'
373-  ORDER BY (milestone IS NULL), milestone DESC, (status = 'closed'),
374+  ORDER BY (IFNULL(milestone, '') = '') DESC, milestone DESC, (status = 'closed'),
375         (CASE status WHEN 'closed' THEN modified ELSE -p.value END) DESC
376 """),
377 #----------------------------------------------------------------------------
378@@ -308,12 +309,12 @@
379 """
380 SELECT p.value AS __color__,
381    (CASE status WHEN 'assigned' THEN 'Assigned' ELSE 'Owned' END) AS __group__,
382-   id AS ticket, summary, component, version, milestone,
383+   id AS ticket, summary, component, status, version, milestone,
384    severity, priority, time AS created,
385    changetime AS _changetime, description AS _description,
386    reporter AS _reporter
387   FROM ticket t, enum p
388-  WHERE t.status IN ('new', 'assigned', 'reopened')
389+  WHERE t.status <> 'closed'
390 AND p.name = t.priority AND p.type = 'priority' AND owner = '$USER'
391   ORDER BY (status = 'assigned') DESC, p.value, milestone, severity, time
392 """),
393@@ -338,6 +339,173 @@
394   WHERE status IN ('new', 'assigned', 'reopened')
395 AND p.name = t.priority AND p.type = 'priority'
396   ORDER BY (owner = '$USER') DESC, p.value, milestone, severity, time
397+"""),
398+#----------------------------------------------------------------------------
399+('Open Tickets, Mine first',
400+"""
401+ * List all not closed tickets by priority.
402+ * Show all tickets owned by the logged in user in a group first.
403+""",
404+"""
405+SELECT p.value AS __color__,
406+   (CASE owner
407+     WHEN '$USER' THEN 'My Tickets'
408+     ELSE 'Open Tickets'
409+    END) AS __group__,
410+   id AS ticket, summary, component, status, version, milestone, severity,
411+   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner,
412+   time AS created,
413+   changetime AS _changetime, description AS _description,
414+   reporter AS _reporter
415+  FROM ticket t, enum p
416+  WHERE status <> 'closed'
417+AND p.name = t.priority AND p.type = 'priority'
418+  ORDER BY (owner = '$USER') DESC, p.value, milestone, severity, time
419+"""),
420+#----------------------------------------------------------------------------
421+('Open Tickets by Version',
422+"""
423+ * List all not closed tickets by priority.
424+ * Group results by version.
425+""",
426+"""
427+SELECT p.value AS __color__,
428+   (CASE WHEN IFNULL(version, '') = '' THEN 'Not Specified' ELSE 'Version ' || version END) AS __group__,
429+   id AS ticket, summary, component, status, milestone, severity,
430+   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner,
431+   time AS created,
432+   changetime AS _changetime, description AS _description,
433+   reporter AS _reporter
434+  FROM ticket t, enum p
435+  WHERE status <> 'closed'
436+AND p.name = t.priority AND p.type = 'priority'
437+  ORDER BY (IFNULL(version, '') = '') desc,version, p.value, severity, time
438+"""),
439+#----------------------------------------------------------------------------
440+('Open Tickets by Milestone',
441+"""
442+ * List all not closed tickets by priority.
443+ * Group results by milestone.
444+""",
445+"""
446+SELECT p.value AS __color__,
447+   (CASE WHEN IFNULL(milestone, '') = '' THEN 'Not Assigned' ELSE milestone||' Release' END) AS __group__,
448+   id AS ticket, summary, component, status, version, severity,
449+   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner,
450+   time AS created,
451+   changetime AS _changetime, description AS _description,
452+   reporter AS _reporter
453+  FROM ticket t, enum p
454+  WHERE status <> 'closed'
455+AND p.name = t.priority AND p.type = 'priority'
456+  ORDER BY (IFNULL(milestone, '') = '') DESC,milestone, p.value, severity, time
457+"""),
458+#----------------------------------------------------------------------------
459+('Open Tickets by Owner',
460+"""
461+List not closed tickets, group by ticket owner, sorted by priority.
462+""",
463+"""
464+SELECT p.value AS __color__,
465+   (CASE WHEN IFNULL(owner, '') = '' THEN 'Not Assigned' ELSE owner END) AS __group__,
466+   id AS ticket, summary, component, status, version, milestone, severity,
467+   time AS created, changetime AS _changetime, description AS _description,
468+   reporter AS _reporter
469+  FROM ticket t,enum p
470+  WHERE status <> 'closed'
471+AND p.name=t.priority AND p.type='priority'
472+  ORDER BY (IFNULL(owner, '') = '') DESC, owner, p.value, severity, time
473+"""),
474+#----------------------------------------------------------------------------
475+('Open Tickets by Status',
476+"""
477+ * List all not closed tickets by priority.
478+ * Group results by status.
479+""",
480+"""
481+SELECT p.value AS __color__,
482+   status AS __group__,
483+   id AS ticket, summary, component, version, milestone, severity, owner,
484+   time AS created,
485+   changetime AS _changetime, description AS _description,
486+   reporter AS _reporter
487+  FROM ticket t, enum q, enum p
488+  WHERE status <> 'closed'
489+AND q.name = t.status AND q.type = 'status'
490+AND p.name = t.priority AND p.type = 'priority'
491+  ORDER BY q.value, p.value, severity, time
492+"""),
493+#----------------------------------------------------------------------------
494+('Resolved Tickets, Mine first',
495+"""
496+ * List all resolved tickets by priority.
497+ * Show all tickets owned by the logged in user in a group first.
498+""",
499+"""
500+SELECT p.value AS __color__,
501+   (CASE owner
502+     WHEN '$USER' THEN 'My Tickets'
503+     ELSE 'Active Tickets'
504+    END) AS __group__,
505+   id AS ticket, summary, component, version, milestone, severity,
506+   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner,
507+   time AS created,
508+   changetime AS _changetime, description AS _description,
509+   reporter AS _reporter
510+  FROM ticket t, enum p
511+  WHERE status = 'resolved'
512+AND p.name = t.priority AND p.type = 'priority'
513+  ORDER BY (owner = '$USER') DESC, p.value, milestone, severity, time
514+"""),
515+#----------------------------------------------------------------------------
516+('Resolved Tickets by Milestone',
517+"""
518+List resolved tickets, sorted by priority, grouped by milestone
519+""",
520+"""
521+SELECT p.value AS __color__,
522+   (CASE WHEN IFNULL(milestone, '') = '' THEN 'Not Assigned' ELSE milestone||' Release' END) AS __group__,
523+   id AS ticket, summary, component, version, severity,
524+   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner,
525+   time AS created,
526+   changetime AS _changetime, description AS _description,
527+   reporter AS _reporter
528+  FROM ticket t, enum p
529+  WHERE status = 'resolved'
530+AND p.name = t.priority AND p.type = 'priority'
531+  ORDER BY (IFNULL(milestone, '') = '') DESC,milestone, p.value, severity, time
532+"""),
533+#----------------------------------------------------------------------------
534+('Resolved Tickets by Owner',
535+"""
536+List resolved tickets, group by ticket owner, sorted by priority.
537+""",
538+"""
539+SELECT p.value AS __color__,
540+   (CASE WHEN IFNULL(owner, '') = '' THEN 'Not Assigned' ELSE owner END) AS __group__,
541+   id AS ticket, summary, component, version, milestone, severity, time AS created,
542+   changetime AS _changetime, description AS _description,
543+   reporter AS _reporter
544+  FROM ticket t,enum p
545+  WHERE status = 'resolved'
546+AND p.name=t.priority AND p.type='priority'
547+  ORDER BY (IFNULL(owner, '') = '') DESC, owner, p.value, severity, time
548+"""),
549+#----------------------------------------------------------------------------
550+('Completed Tickets by Milestone (Full Description)',
551+"""
552+Release Notes: List verified and closed tickets, group by milestone, include description.
553+""",
554+"""
555+SELECT p.value AS __color__,
556+   (CASE WHEN IFNULL(milestone, '') = '' THEN 'Not Assigned' ELSE milestone||' Release' END) AS __group__,
557+   id AS ticket, summary, component, status, version, severity, time AS created,
558+   description AS _description_,
559+   changetime AS _changetime, reporter AS _reporter
560+  FROM ticket t, enum p
561+  WHERE status IN ('verified', 'closed')
562+AND p.name = t.priority AND p.type = 'priority'
563+  ORDER BY (IFNULL(milestone, '') = '') DESC,milestone, p.value, severity, time
564 """))
565 
566 
567@@ -347,9 +515,9 @@
568 
569 # (table, (column1, column2), ((row1col1, row1col2), (row2col1, row2col2)))
570 data = (('component',
571-             ('name', 'owner'),
572-               (('component1', 'somebody'),
573-                ('component2', 'somebody'))),
574+             ('name', 'owner', 'qaowner'),
575+               (('component1', 'somebody', 'qasomebody'),
576+                ('component2', 'somebody', 'qasomebody'))),
577            ('milestone',
578              ('name', 'time'),
579                (('', 0),
580@@ -367,7 +535,9 @@
581                (('status', 'new', 1),
582                 ('status', 'assigned', 2),
583                 ('status', 'reopened', 3),
584-                ('status', 'closed', 4),
585+                ('status', 'resolved', 4),
586+                ('status', 'verified', 5),
587+                ('status', 'closed', 6),
588                 ('resolution', 'fixed', 1),
589                 ('resolution', 'invalid', 2),
590                 ('resolution', 'wontfix', 3),
591@@ -426,6 +596,7 @@
592   ('project', 'footer',
593    ' Visit the Trac open source project at<br />'
594    '<a href="http://trac.edgewall.com/">http://trac.edgewall.com/</a>'),
595+  ('ticket', 'workflow', 'trac.workflows.QaRmtWorkflow'),
596   ('ticket', 'default_version', ''),
597   ('ticket', 'default_severity', 'normal'),
598   ('ticket', 'default_priority', 'normal'),
599Index: trac/Milestone.py
600===================================================================
601--- trac/Milestone.py   (revision 1098)
602+++ trac/Milestone.py   (working copy)
603@@ -60,10 +60,10 @@
604     if not group:
605         queries['all_tickets'] = env.href.query({'milestone': milestone})
606         queries['active_tickets'] = env.href.query({
607-            'milestone': milestone, 'status': ['new', 'assigned', 'reopened']
608+            'milestone': milestone, 'status': ['new', 'assigned', 'reopened', 'resolved']
609         })
610         queries['closed_tickets'] = env.href.query({
611-            'milestone': milestone, 'status': 'closed'
612+            'milestone': milestone, 'status': ['closed', 'verified']
613         })
614     else:
615         queries['all_tickets'] = env.href.query({
616@@ -71,17 +71,17 @@
617         })
618         queries['active_tickets'] = env.href.query({
619             'milestone': milestone, grouped_by: group,
620-            'status': ['new', 'assigned', 'reopened']
621+            'status': ['new', 'assigned', 'reopened', 'resolved']
622         })
623         queries['closed_tickets'] = env.href.query({
624             'milestone': milestone, grouped_by: group,
625-            'status': 'closed'
626+            'status': ['closed', 'verified']
627         })
628     return queries
629 
630 def calc_ticket_stats(tickets):
631     total_cnt = len(tickets)
632-    active = [ticket for ticket in tickets if ticket['status'] != 'closed']
633+    active = [ticket for ticket in tickets if ticket['status'] != 'closed' and ticket['status'] != 'verified']
634     active_cnt = len(active)
635     closed_cnt = total_cnt - active_cnt
636 
637@@ -116,10 +116,11 @@
638                 if datestr:
639                     date = self.parse_date(datestr)
640             descr = self.args.get('descr', '')
641+            owner = self.args.get('owner', '')
642             if not id:
643-                self.create_milestone(name, date, descr)
644+                self.create_milestone(name, date, descr, owner)
645             else:
646-                self.update_milestone(id, name, date, descr)
647+                self.update_milestone(id, name, date, descr, owner)
648         elif id:
649             self.req.redirect(self.env.href.milestone(id))
650         else:
651@@ -141,15 +142,15 @@
652                             'Invalid Date Format')
653         return seconds
654 
655-    def create_milestone(self, name, date=0, descr=''):
656+    def create_milestone(self, name, date=0, descr='', owner=''):
657         self.perm.assert_permission(perm.MILESTONE_CREATE)
658         if not name:
659             raise TracError('You must provide a name for the milestone.',
660                             'Required Field Missing')
661         cursor = self.db.cursor()
662         self.log.debug("Creating new milestone '%s'" % name)
663-        cursor.execute("INSERT INTO milestone (id, name, time, descr) "
664-                       "VALUES (NULL, %s, %d, %s)", name, date, descr)
665+        cursor.execute("INSERT INTO milestone (id, name, time, descr, owner) "
666+                       "VALUES (NULL, %s, %d, %s, %s)", name, date, descr, owner)
667         self.db.commit()
668         self.req.redirect(self.env.href.milestone(name))
669 
670@@ -178,7 +179,7 @@
671         else:
672             self.req.redirect(self.env.href.milestone(id))
673 
674-    def update_milestone(self, id, name, date, descr):
675+    def update_milestone(self, id, name, date, descr, owner):
676         self.perm.assert_permission(perm.MILESTONE_MODIFY)
677         cursor = self.db.cursor()
678         self.log.info("Updating milestone '%s'" % id)
679@@ -188,8 +189,8 @@
680             cursor.execute('UPDATE ticket SET milestone = %s '
681                             'WHERE milestone = %s', name, id)
682             cursor.execute("UPDATE milestone SET name = %s, time = %d, "
683-                           "descr = %s WHERE name = %s",
684-                           name, date, descr, id)
685+                           "descr = %s, owner = %s WHERE name = %s",
686+                           name, date, descr, owner, id)
687             self.db.commit()
688             self.req.redirect(self.env.href.milestone(name))
689         else:
690@@ -222,7 +223,7 @@
691 
692     def get_milestone(self, name):
693         cursor = self.db.cursor()
694-        cursor.execute("SELECT name, time, descr FROM milestone "
695+        cursor.execute("SELECT name, time, descr, owner FROM milestone "
696                        "WHERE name = %s ORDER BY time, name", name)
697         row = cursor.fetchone()
698         cursor.close()
699@@ -237,6 +238,7 @@
700         t = row['time'] and int(row['time'])
701         if t > 0:
702             milestone['date'] = time.strftime('%x', time.localtime(t))
703+        milestone['owner'] = row['owner'] or ''
704         return milestone
705 
706     def render(self):
707Index: trac/Timeline.py
708===================================================================
709--- trac/Timeline.py    (revision 1098)
710+++ trac/Timeline.py    (working copy)
711@@ -52,6 +52,9 @@
712         REOPENED_TICKET = 4
713         WIKI = 5
714         MILESTONE = 6
715+        VERIFIED_TICKET = 7
716+        RESOLVED_TICKET = 8
717+        RETESTED_TICKET = 9
718 
719         q = []
720         if changeset:
721@@ -60,26 +63,65 @@
722                      "FROM revision WHERE time>=%s AND time<=%s" %
723                      (start, stop))
724         if tickets:
725+            # New tickets
726             q.append("SELECT time, id AS idata, '' AS tdata, 2 AS type, "
727                      "summary AS message, reporter AS author "
728                      "FROM ticket WHERE time>=%s AND time<=%s" %
729                      (start, stop))
730-            q.append("SELECT time, ticket AS idata, '' AS tdata, 4 AS type, "
731-                     "'' AS message, author "
732-                     "FROM ticket_change WHERE field='status' "
733-                     "AND newvalue='reopened' AND time>=%s AND time<=%s" %
734-                     (start, stop))
735+            # Reopened tickets
736             q.append("SELECT t1.time AS time, t1.ticket AS idata,"
737+                     "       '' AS tdata, 4 AS type,"
738+                     "       t3.newvalue AS message, t1.author AS author"
739+                     " FROM ticket_change t1"
740+                     "   LEFT OUTER JOIN ticket_change t3 ON t1.time = t3.time"
741+                     "     AND t1.ticket = t3.ticket AND t3.field = 'comment'"
742+                     " WHERE t1.field = 'status' AND t1.newvalue = 'reopened'"
743+                     "   AND t1.time >= %s AND t1.time <= %s" % (start,stop))
744+            # Closed tickets (including resolution field for old workflow)
745+            q.append("SELECT t1.time AS time, t1.ticket AS idata,"
746                      "       t2.newvalue AS tdata, 3 AS type,"
747                      "       t3.newvalue AS message, t1.author AS author"
748                      " FROM ticket_change t1"
749-                     "   INNER JOIN ticket_change t2 ON t1.ticket = t2.ticket"
750-                     "     AND t1.time = t2.time"
751+                     "   LEFT OUTER JOIN ticket_change t2 ON t1.ticket = t2.ticket"
752+                     "     AND t1.time = t2.time AND t2.field = 'resolution'"
753                      "   LEFT OUTER JOIN ticket_change t3 ON t1.time = t3.time"
754                      "     AND t1.ticket = t3.ticket AND t3.field = 'comment'"
755                      " WHERE t1.field = 'status' AND t1.newvalue = 'closed'"
756-                     "   AND t2.field = 'resolution'"
757                      "   AND t1.time >= %s AND t1.time <= %s" % (start,stop))
758+            # Verified tickets (including resolution field for customized workflows)
759+            q.append("SELECT t1.time AS time, t1.ticket AS idata,"
760+                     "       t2.newvalue AS tdata, 7 AS type,"
761+                     "       t3.newvalue AS message, t1.author AS author"
762+                     " FROM ticket_change t1"
763+                     "   LEFT OUTER JOIN ticket_change t2 ON t1.ticket = t2.ticket"
764+                     "     AND t1.time = t2.time AND t2.field = 'resolution'"
765+                     "   LEFT OUTER JOIN ticket_change t3 ON t1.time = t3.time"
766+                     "     AND t1.ticket = t3.ticket AND t3.field = 'comment'"
767+                     " WHERE t1.field = 'status' AND t1.newvalue = 'verified'"
768+                     "   AND t1.oldvalue<>'closed'"
769+                     "   AND t1.time >= %s AND t1.time <= %s" % (start,stop))
770+            # Resolved tickets (including resolution field)
771+            q.append("SELECT t1.time AS time, t1.ticket AS idata,"
772+                     "       t2.newvalue AS tdata, 8 AS type,"
773+                     "       t3.newvalue AS message, t1.author AS author"
774+                     " FROM ticket_change t1"
775+                     "   LEFT OUTER JOIN ticket_change t2 ON t1.ticket = t2.ticket"
776+                     "     AND t1.time = t2.time AND t2.field = 'resolution'"
777+                     "   LEFT OUTER JOIN ticket_change t3 ON t1.time = t3.time"
778+                     "     AND t1.ticket = t3.ticket AND t3.field = 'comment'"
779+                     " WHERE t1.field = 'status' AND t1.newvalue = 'resolved'"
780+                     "   AND t1.oldvalue NOT IN ('verified', 'closed')"
781+                     "   AND t1.time >= %s AND t1.time <= %s" % (start,stop))
782+            # Retested tickets
783+            q.append("SELECT t1.time AS time, t1.ticket AS idata,"
784+                     "       '' AS tdata, 9 AS type,"
785+                     "       t3.newvalue AS message, t1.author AS author"
786+                     " FROM ticket_change t1"
787+                     "   LEFT OUTER JOIN ticket_change t3 ON t1.time = t3.time"
788+                     "     AND t1.ticket = t3.ticket AND t3.field = 'comment'"
789+                     " WHERE t1.field = 'status' AND t1.newvalue = 'resolved'"
790+                     "   AND t1.oldvalue IN ('verified', 'closed')"
791+                     "   AND t1.time >= %s AND t1.time <= %s" % (start,stop))
792         if wiki:
793             q.append("SELECT time, -1 AS idata, name AS tdata, 5 AS type, "
794                      "comment AS message, author "
795@@ -87,7 +129,7 @@
796                      (start, stop))
797         if milestone:
798             q.append("SELECT time, -1 AS idata, '' AS tdata, 6 AS type, "
799-                     "name AS message, '' AS author "
800+                     "name AS message, owner AS author "
801                      "FROM milestone WHERE time>=%s AND time<=%s" %
802                      (start, stop))
803 
804@@ -110,7 +152,7 @@
805                     'date': time.strftime('%x', t),
806                     'datetime': time.strftime('%a, %d %b %Y %H:%M:%S GMT', gmt),
807                     'idata': int(row['idata']),
808-                    'tdata': row['tdata'],
809+                    'tdata': row['tdata'] or '',
810                     'type': int(row['type']),
811                     'message': row['message'] or '',
812                     'author': util.escape(row['author'] or 'anonymous')
813Index: trac/workflows/Base.py
814===================================================================
815--- trac/workflows/Base.py      (revision 0)
816+++ trac/workflows/Base.py      (revision 0)
817@@ -0,0 +1,87 @@
818+# -*- coding: iso8859-1 -*-
819+#
820+# Copyright (C) 2003, 2004 Edgewall Software
821+# Copyright (C) 2003, 2004 Jonas Borgstr�jonas@edgewall.com>
822+#
823+# Trac is free software; you can redistribute it and/or
824+# modify it under the terms of the GNU General Public License as
825+# published by the Free Software Foundation; either version 2 of the
826+# License, or (at your option) any later version.
827+#
828+# Trac is distributed in the hope that it will be useful,
829+# but WITHOUT ANY WARRANTY; without even the implied warranty of
830+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
831+# General Public License for more details.
832+#
833+# You should have received a copy of the GNU General Public License
834+# along with this program; if not, write to the Free Software
835+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
836+#
837+# Author: Pavel Kourochka <pkou@ua.fm>
838+#
839+# Abstract workflow definition
840+
841+class WorkflowBase:
842+    """
843+    Generic workflow class for Trac.
844+    """
845+
846+    def __init__(self, env, db, user):
847+        """
848+        Constructor for workflow class.
849+        """
850+        self.env = env
851+        self.db = db
852+        self.user = user
853+
854+    def get_actions(self, ticket):
855+        """
856+        For existing tickets only.
857+        Return the list of available actions for specified ticket.
858+        """
859+        raise Exception, "WorkflowBase::get_actions not implemented"
860+
861+    def do_action(self, ticket, action, args):
862+        """
863+        For new and existing tickets.
864+        Perform action on a ticket.  For new tickets, action name is 'create'.
865+        """
866+        raise Exception, "WorkflowBase::do_action not implemented"
867+
868+    def get_actions_template(self, ticket):
869+        """
870+        For new and existing tickets.
871+        Return the name of ClearSilver template file for the workflow.
872+        Return None if no additional template is required.
873+        """
874+        return None
875+
876+    def init_template(self, ticket, hdf):
877+        """
878+        For new and existing tickets.
879+        Initialize ClearSilver variables for actions template.
880+        Called if get_actions_template() returns file name only.
881+        """
882+        pass
883+
884+    def validate(self, ticket):
885+        """
886+        For new and existing tickets.
887+        Validate ticket.
888+        Return list of Wiki strings that describe errors in the ticket.
889+        """
890+        return []
891+
892+    def on_insert(self, ticket):
893+        """
894+        For new tickets only.
895+        Update ticket fields just before inserting the ticket into database.
896+        """
897+        pass
898+
899+    def on_update(self, ticket):
900+        """
901+        For existing tickets only.
902+        Update ticket fields just before saving the ticket into database.
903+        """
904+        pass
905Index: trac/workflows/QaRmtWorkflow.py
906===================================================================
907--- trac/workflows/QaRmtWorkflow.py     (revision 0)
908+++ trac/workflows/QaRmtWorkflow.py     (revision 0)
909@@ -0,0 +1,132 @@
910+# -*- coding: iso8859-1 -*-
911+#
912+# Copyright (C) 2003, 2004 Edgewall Software
913+# Copyright (C) 2003, 2004 Jonas Borgstr�jonas@edgewall.com>
914+#
915+# Trac is free software; you can redistribute it and/or
916+# modify it under the terms of the GNU General Public License as
917+# published by the Free Software Foundation; either version 2 of the
918+# License, or (at your option) any later version.
919+#
920+# Trac is distributed in the hope that it will be useful,
921+# but WITHOUT ANY WARRANTY; without even the implied warranty of
922+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
923+# General Public License for more details.
924+#
925+# You should have received a copy of the GNU General Public License
926+# along with this program; if not, write to the Free Software
927+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
928+#
929+# Author: Pavel Kourochka <pkou@ua.fm>
930+#
931+# Workflow definition for development, QA, and release management teams
932+
933+from trac.workflows.SimpleWorkflow import SimpleWorkflow
934+
935+class QaRmtWorkflow(SimpleWorkflow):
936+
937+    def get_actions(self, ticket):
938+        actions = {
939+ 'new':      ['leave','reassign','resolve',                          'accept'],
940+ 'assigned': ['leave','reassign','resolve',                                  ],
941+ 'reopened': ['leave','reassign','resolve',                                  ],
942+ 'resolved': ['leave','reassign',          'reopen',         'close','verify'],
943+ 'verified': ['leave','reassign',          'reopen','retest','close'         ],
944+ 'closed':   ['leave',                     'reopen','retest'                 ]
945+        }
946+        return actions.get(ticket['status'], ['leave'])
947+
948+    def do_action(self, ticket, action, args):
949+        if action == 'accept':
950+            ticket['status'] = 'assigned'
951+            ticket['owner'] = self.user
952+        elif action == 'resolve':
953+            ticket['status'] = 'resolved'
954+            ticket['resolution'] = args.get('resolve_resolution')
955+            ticket['owner'] = ''
956+        elif action == 'verify':
957+            ticket['status'] = 'verified'
958+            ticket['owner'] = ''
959+        elif action == 'close':
960+            ticket['status'] = 'closed'
961+        elif action == 'reassign':
962+            newowner = args.get('reassign_owner')
963+            if ticket['owner'] != newowner:
964+                if ticket['status'] == 'assigned': ticket['status'] = 'new'
965+                ticket['owner'] = newowner
966+        elif action == 'reopen':
967+            ticket['status'] = 'reopened'
968+            ticket['resolution'] = ''
969+            ticket['owner'] = ''
970+        elif action == 'retest':
971+            ticket['status'] = 'resolved'
972+            ticket['owner'] = ''
973+
974+    def get_actions_template(self, ticket):
975+        if ticket.has_key('id'):
976+            return 'ticket_workflow_qarmt.cs'
977+        else:
978+            return None
979+
980+    def on_insert(self, ticket):
981+        SimpleWorkflow.on_insert(self, ticket)
982+
983+        # The owner field defaults to the milestone owner if
984+        # the component does not have any owner
985+        cursor = self.db.cursor()
986+        if ticket.get('owner', '') == '':
987+            cursor.execute('SELECT owner FROM milestone '
988+                           'WHERE name=%s', ticket.get('milestone', ''))
989+            ticket['owner'] = cursor.fetchone()[0] or ''
990+
991+    def on_update(self, ticket):
992+        SimpleWorkflow.on_update(self, ticket)
993+        if not ticket._old: return # Not modified
994+
995+        cursor = self.db.cursor()
996+        status = ticket.get('status', 'new')
997+        component = ticket.get('component', '')
998+        milestone = ticket.get('milestone', '')
999+
1000+        # If the milestone is changed on a 'new' ticket then owner field
1001+        # is updated accordingly if the component does not have any owner.
1002+        # (related to #623).
1003+        if status == 'new' and ticket._old.has_key('milestone') and \
1004+               not ticket._old.has_key('component') and \
1005+               not ticket._old.has_key('owner'):
1006+            cursor.execute('SELECT owner FROM component '
1007+                           'WHERE name=%s', component)
1008+            if not cursor.fetchone()[0]:
1009+                cursor.execute('SELECT owner FROM milestone '
1010+                               'WHERE name=%s', ticket._old['milestone'])
1011+                old_owner = cursor.fetchone()[0]
1012+                if ticket['owner'] == old_owner:
1013+                    cursor.execute('SELECT owner FROM milestone '
1014+                                   'WHERE name=%s', milestone)
1015+                    ticket['owner'] = cursor.fetchone()[0] or ''
1016+
1017+        # 1. The owner field defaults to the component owner for active tickets
1018+        if ticket.get('owner', '') == '' and status in ['new', 'reopened']:
1019+            cursor.execute('SELECT owner FROM component '
1020+                           'WHERE name=%s', component)
1021+            newowner = cursor.fetchone()[0]
1022+            if newowner: ticket['owner'] = newowner
1023+
1024+        # 2. The owner field defaults to component QA owner for testing tickets
1025+        if ticket.get('owner', '') == '' and status == 'resolved':
1026+            cursor.execute('SELECT qaowner FROM component '
1027+                           'WHERE name=%s', component)
1028+            newowner = cursor.fetchone()[0]
1029+            if newowner: ticket['owner'] = newowner
1030+
1031+        # 3. The owner field defaults to milestone owner for open tickets
1032+        if ticket.get('owner', '') == '' and status != 'closed':
1033+            cursor.execute('SELECT owner FROM milestone '
1034+                           'WHERE name=%s', milestone)
1035+            newowner = cursor.fetchone()[0]
1036+            if newowner: ticket['owner'] = newowner
1037+
1038+        # 4. The owner field defaults to reporter for verified tickets
1039+        if ticket.get('owner', '') == '' and status == 'verified':
1040+            reporter = ticket.get('reporter', '')
1041+            if reporter: ticket['owner'] = reporter
1042Index: trac/workflows/__init__.py
1043===================================================================
1044--- trac/workflows/__init__.py  (revision 0)
1045+++ trac/workflows/__init__.py  (revision 0)
1046@@ -0,0 +1 @@
1047+__all__ = ['Base', 'SimpleWorkflow', 'QaRmtWorkflow']
1048Index: trac/workflows/SimpleWorkflow.py
1049===================================================================
1050--- trac/workflows/SimpleWorkflow.py    (revision 0)
1051+++ trac/workflows/SimpleWorkflow.py    (revision 0)
1052@@ -0,0 +1,94 @@
1053+# -*- coding: iso8859-1 -*-
1054+#
1055+# Copyright (C) 2003, 2004 Edgewall Software
1056+# Copyright (C) 2003, 2004 Jonas Borgstr�jonas@edgewall.com>
1057+#
1058+# Trac is free software; you can redistribute it and/or
1059+# modify it under the terms of the GNU General Public License as
1060+# published by the Free Software Foundation; either version 2 of the
1061+# License, or (at your option) any later version.
1062+#
1063+# Trac is distributed in the hope that it will be useful,
1064+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1065+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
1066+# General Public License for more details.
1067+#
1068+# You should have received a copy of the GNU General Public License
1069+# along with this program; if not, write to the Free Software
1070+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
1071+#
1072+# Author: Pavel Kourochka <pkou@ua.fm>
1073+#
1074+# Simple workflow definition (as in Trac 0.8)
1075+
1076+from trac.workflows.Base import WorkflowBase
1077+
1078+class SimpleWorkflow(WorkflowBase):
1079+
1080+    def get_actions(self, ticket):
1081+        actions = {
1082+            'new':      ['leave', 'resolve', 'reassign', 'accept'],
1083+            'assigned': ['leave', 'resolve', 'reassign'          ],
1084+            'reopened': ['leave', 'resolve', 'reassign'          ],
1085+            'closed':   ['leave',                        'reopen']
1086+        }
1087+        return actions.get(ticket['status'], ['leave'])
1088+
1089+    def do_action(self, ticket, action, args):
1090+        if action == 'accept':
1091+            ticket['status'] = 'assigned'
1092+            ticket['owner'] = self.user
1093+        elif action == 'resolve':
1094+            ticket['status'] = 'closed'
1095+            ticket['resolution'] = args.get('resolve_resolution')
1096+        elif action == 'reassign':
1097+            ticket['owner'] = args.get('reassign_owner')
1098+            ticket['status'] = 'new'
1099+        elif action == 'reopen':
1100+            ticket['status'] = 'reopened'
1101+            ticket['resolution'] = ''
1102+
1103+    def get_actions_template(self, ticket):
1104+        if ticket.has_key('id'):
1105+            return 'ticket_workflow_simple.cs'
1106+        else:
1107+            return None
1108+
1109+    def init_template(self, ticket, hdf):
1110+        WorkflowBase.init_template(self, ticket, hdf)
1111+        if ticket.has_key('id'):
1112+            for a in self.get_actions(ticket):
1113+                hdf.setValue('ticket.workflow.action.' + a, '1')
1114+
1115+    def validate(self, ticket):
1116+        err = WorkflowBase.validate(self, ticket)
1117+        if not ticket.get('summary'):
1118+            err.append("The ticket must contain '''Summary''' field.")
1119+        return err
1120+
1121+    def on_insert(self, ticket):
1122+        WorkflowBase.on_insert(self, ticket)
1123+
1124+        # The owner field defaults to the component owner
1125+        cursor = self.db.cursor()
1126+        if ticket.get('owner', '') == '':
1127+            cursor.execute('SELECT owner FROM component '
1128+                           'WHERE name=%s', ticket.get('component', ''))
1129+            ticket['owner'] = cursor.fetchone()[0] or ''
1130+
1131+    def on_update(self, ticket):
1132+        WorkflowBase.on_update(self, ticket)
1133+        if not ticket._old: return # Not modified
1134+
1135+        # If the component is changed on a 'new' ticket then owner field
1136+        # is updated accordingly. (#623).
1137+        cursor = self.db.cursor()
1138+        if ticket['status'] == 'new' and ticket._old.has_key('component') and \
1139+               not ticket._old.has_key('owner'):
1140+            cursor.execute('SELECT owner FROM component '
1141+                           'WHERE name=%s', ticket._old['component'])
1142+            old_owner = cursor.fetchone()[0]
1143+            if ticket['owner'] == old_owner:
1144+                cursor.execute('SELECT owner FROM component '
1145+                               'WHERE name=%s', ticket['component'])
1146+                ticket['owner'] = cursor.fetchone()[0] or ''
1147Index: trac/upgrades/db8.py
1148===================================================================
1149--- trac/upgrades/db8.py        (revision 0)
1150+++ trac/upgrades/db8.py        (revision 0)
1151@@ -0,0 +1,275 @@
1152+sql = """
1153+-- Add statuses 'resolved' and 'verified'
1154+UPDATE enum SET value = 6 WHERE type = 'status' AND name = 'closed';
1155+INSERT INTO enum (type, name, value) VALUES ('status', 'resolved', 4);
1156+INSERT INTO enum (type, name, value) VALUES ('status', 'verified', 5);
1157+
1158+-- Add QA Contact to 'component'
1159+CREATE TEMPORARY TABLE component_backup AS SELECT * FROM component;
1160+DROP TABLE component;
1161+CREATE TABLE component (