Edgewall Software

NewWorkflow: patch-newworkflow-r1064.diff

File patch-newworkflow-r1064.diff, 89.2 KB (added by pkou <pkou at ua.fm>, 7 years ago)

Support customized workflows in Trac

  • htdocs/css/timeline.css

     
    3030/* Apply icon background-image twice to avoid hover-flicker in IE/Win */ 
    3131dt.changeset, dt.changeset a { background-image: url(../changeset.png) !important } 
    3232dt.newticket, dt.newticket a { background-image: url(../newticket.png) !important } 
     33dt.resolvedticket, dt.resolvedticket a { background-image: url(../resolvedticket.png) !important } 
     34dt.reopenedticket, dt.reopenedticket a { background-image: url(../reopenedticket.png) !important } 
    3335dt.closedticket, dt.closedticket a { background-image: url(../closedticket.png) !important } 
    3436dt.wiki, dt.wiki a { background-image: url(../wiki.png) !important } 
    3537dt.milestone, dt.milestone a { background-image: url(../milestone.png) !important } 
  • wiki-default/TracIni

     
    2222See also: TracLogging 
    2323 
    2424== [ticket] == 
     25|| workflow || Ticket workflow class.  If not specified, it is ''trac.workflows.SimpleWorkflow'' || 
    2526|| default_version   || Default version for newly created tickets || 
    2627|| default_severity  || Default severity for newly created tickets || 
    2728|| default_priority  || Default priority for newly created tickets || 
     
    5455  
    5556[[BR]] 
    5657---- 
    57 See also: TracGuide, TracAdmin 
    58  No newline at end of file 
     58See also: TracGuide, TracAdmin 
  • wiki-default/TracAdmin

     
    2323permission add <user> <action>      -- Add a new permission rule 
    2424permission remove <user> <action>   -- Remove permission rule 
    2525component list                      -- Show available components 
    26 component add <name> <owner>        -- Add a new component 
     26component add <name> <owner> [<qaowner>]   -- Add a new component 
    2727component rename <name> <newname>   -- Rename a component 
    2828component remove <name>             -- Remove/uninstall component 
    29 component chown <name> <owner>     -- Change component ownership 
     29component chown <name> <owner> [<qaowner>] -- Change component ownership 
    3030priority list                       -- Show possible ticket priorities 
    3131priority add <value>                -- Add a priority value option 
    3232priority change <value> <newvalue>  -- Change a priority value 
     
    4141version time <name> <time>          -- Set version date/time 
    4242version remove <name>               -- Remove version 
    4343milestone list                      -- Show milestones 
    44 milestone add <name> [time]        -- Add milestone 
     44milestone add <name> [<owner> [time]] -- Add milestone 
    4545milestone rename <name> <newname>   -- Rename milestone 
    4646milestone time <name> <time>        -- Set milestone date/time 
     47milestone chown <name> <owner>      -- Change milestone ownership 
    4748milestone remove <name>             -- Remove milestone 
    4849}}} 
    4950 
  • scripts/trac-admin

     
    290290     
    291291#    ## Component 
    292292    _help_component = [('component list', 'Show available components'), 
    293                        ('component add <name> <owner>', 'Add a new component'), 
     293                       ('component add <name> <owner> [<qaowner>]', 'Add a new component'), 
    294294                       ('component rename <name> <newname>', 'Rename a component'), 
    295295                       ('component remove <name>', 'Remove/uninstall component'), 
    296                        ('component chown <name> <owner>', 'Change component ownership')] 
     296                       ('component chown <name> <owner> [<qaowner>]', 'Change component ownership')] 
    297297 
    298298    def complete_component (self, text, line, begidx, endidx): 
    299299        if begidx in [16,17]: 
     
    309309        try: 
    310310            if arg[0]  == 'list': 
    311311                self._do_component_list() 
    312             elif arg[0] == 'add' and len(arg)==3: 
     312            elif arg[0] == 'add' and len(arg) in [3,4]: 
    313313                name = arg[1] 
    314314                owner = arg[2] 
    315                 self._do_component_add(name, owner) 
     315                if len(arg) == 4: 
     316                    qaowner = arg[3] 
     317                else: 
     318                    qaowner = owner 
     319                self._do_component_add(name, owner, qaowner) 
    316320            elif arg[0] == 'rename' and len(arg)==3: 
    317321                name = arg[1] 
    318322                newname = arg[2] 
     
    320324            elif arg[0] == 'remove'  and len(arg)==2: 
    321325                name = arg[1] 
    322326                self._do_component_remove(name) 
    323             elif arg[0] == 'chown' and len(arg)==3: 
     327            elif arg[0] == 'chown' and len(arg) in [3,4]: 
    324328                name = arg[1] 
    325329                owner = arg[2] 
    326                 self._do_component_set_owner(name, owner) 
     330                if len(arg) == 4: 
     331                    qaowner = arg[3] 
     332                else: 
     333                    qaowner = owner 
     334                self._do_component_set_owner(name, owner, qaowner) 
    327335            else:     
    328336                self.do_help ('component') 
    329337        except Exception, e: 
    330338            print 'Component %s failed:' % arg[0], e 
    331339 
    332340    def _do_component_list(self): 
    333         data = self.db_execsql('SELECT name, owner FROM component')  
    334         self.print_listing(['Name', 'Owner'], data) 
     341        data = self.db_execsql('SELECT name, owner, qaowner FROM component')  
     342        self.print_listing(['Name', 'Owner', 'QA Owner'], data) 
    335343 
    336     def _do_component_add(self, name, owner): 
    337         data = self.db_execsql("INSERT INTO component VALUES('%s', '%s')" 
    338                                % (name, owner)) 
     344    def _do_component_add(self, name, owner, qaowner): 
     345        data = self.db_execsql("INSERT INTO component VALUES('%s', '%s', '%s')" 
     346                               % (name,owner,qaowner)) 
    339347 
    340348    def _do_component_rename(self, name, newname): 
    341349        cnx = self.db_open() 
     
    360368        data = self.db_execsql("DELETE FROM component WHERE name='%s'" 
    361369                               % (name)) 
    362370 
    363     def _do_component_set_owner(self, name, owner): 
     371    def _do_component_set_owner(self, name, owner, qaowner): 
    364372        cnx = self.db_open() 
    365373        cursor = cnx.cursor () 
    366374        cursor.execute('SELECT name FROM component WHERE name=%s', name) 
    367375        data = cursor.fetchone() 
    368376        if not data: 
    369377            raise Exception("No such component '%s'" % name) 
    370         data = self.db_execsql("UPDATE component SET owner='%s' WHERE name='%s'" 
    371                                % (owner,name)) 
     378        data = self.db_execsql("UPDATE component SET owner='%s', qaowner='%s' WHERE name='%s'" 
     379                               % (owner,qaowner,name)) 
    372380 
    373381 
    374382    ## Permission 
     
    795803 
    796804    ## Milestone 
    797805    _help_milestone = [('milestone list', 'Show milestones'), 
    798                        ('milestone add <name> [time]', 'Add milestone'), 
     806                       ('milestone add <name> [<owner> [time]]', 'Add milestone'), 
    799807                       ('milestone rename <name> <newname>', 
    800808                        'Rename milestone'), 
     809                       ('milestone chown <name> <newowner>', 'Change milestone owner'), 
    801810                       ('milestone time <name> <time>', 'Set milestone date (Format: "Jun 3, 2003")'), 
    802811                       ('milestone remove <name>', 'Remove milestone')] 
    803812 
     
    805814 
    806815        if begidx in [15,17]: 
    807816            comp = self.get_milestone_list () 
     817        elif begidx > 15 and line.startswith('milestone chown '): 
     818            comp = self.get_user_list() 
    808819        elif begidx < 15: 
    809             comp = ['list','add','rename','time','remove'] 
     820            comp = ['list','add','rename','chown','time','remove'] 
    810821        return self.word_complete(text, comp) 
    811822 
    812823    def do_milestone(self, line): 
    813         self._do_mile_ver('milestone', line) 
     824        type = 'milestone' 
     825        arg = self.arg_tokenize(line) 
     826        try: 
     827            if arg[0]  == 'list': 
     828                self._do_milestone_list() 
     829            elif arg[0] == 'add' and len(arg) in [2,3,4]: 
     830                name = arg[1] 
     831                self._do_mile_ver_add(type, name) 
     832                if len(arg) >= 3: 
     833                    owner = arg[2] 
     834                    self._do_mile_ver_chown(type, name, owner) 
     835                if len(arg) >= 4: 
     836                    time = arg[3] 
     837                    self._do_mile_ver_time(type, name, time) 
     838            elif arg[0] == 'rename' and len(arg)==3: 
     839                name = arg[1] 
     840                newname = arg[2] 
     841                self._do_mile_ver_rename(type, name, newname) 
     842            elif arg[0] == 'chown' and len(arg)==3: 
     843                name = arg[1] 
     844                owner = arg[2] 
     845                self._do_mile_ver_chown(type, name, owner) 
     846            elif arg[0] == 'time' and len(arg)==3: 
     847                name = arg[1] 
     848                time = arg[2] 
     849                self._do_mile_ver_time(type, name, time) 
     850            elif arg[0] == 'remove' and len(arg)==2: 
     851                name = arg[1] 
     852                self._do_mile_ver_remove(type, name) 
     853            else: 
     854                self.do_help (type) 
     855        except Exception, e: 
     856            print 'Command %s failed:' % arg[0], e 
    814857 
     858    def _do_milestone_list(self): 
     859        data = self.db_execsql("SELECT name,owner,time FROM milestone ORDER BY time,name") 
     860        data = map(lambda x: (x[0], x[1], x[2] and time.strftime('%c', time.localtime(x[2]))), data) 
     861        #print data 
     862        self.print_listing(['Name', 'Owner', 'Time'], data) 
    815863 
     864 
    816865    ## Version 
    817866    _help_version = [('version list', 'Show versions'), 
    818867                       ('version add <name> [time]', 'Add version'), 
     
    832881    def do_version(self, line): 
    833882        self._do_mile_ver('version', line) 
    834883 
    835     # Milestone and Version are identical,  methods 
     884    # Milestone and Version are almost identical,  methods 
    836885 
    837886    def _do_mile_ver(self, type, line): 
    838887        arg = self.arg_tokenize(line) 
     
    923972        else: 
    924973            print >> sys.stderr, 'Unknown time format' 
    925974 
     975    def _do_mile_ver_chown(self, type, name, owner): 
     976        data = self.db_execsql("UPDATE %s SET owner='%s' WHERE name='%s'" 
     977                               % (type, owner, name)); 
     978 
    926979    _help_upgrade = [('upgrade', 'Upgrade database to current version.')] 
    927980    def do_upgrade(self, line): 
    928981        arg = self.arg_tokenize(line) 
  • setup.py

     
    198198      author_email="info@edgewall.com", 
    199199      license=LICENSE, 
    200200      url=URL, 
    201       packages=['trac', 'trac.upgrades', 'trac.wikimacros', 'trac.mimeviewers'], 
     201      packages=['trac', 'trac.upgrades', 'trac.wikimacros', 'trac.mimeviewers', 
     202                'trac.workflows'], 
    202203      data_files=[(_p('share/trac/templates'), glob('templates/*')), 
    203204                  (_p('share/trac/htdocs'), glob(_p('htdocs/*.*')) + [_p('htdocs/README')]), 
    204205                  (_p('share/trac/htdocs/css'), glob(_p('htdocs/css/*'))), 
  • trac/db_default.py

     
    2121 
    2222 
    2323# Database version identifier. Used for automatic upgrades. 
    24 db_version = 7 
     24db_version = 8 
    2525 
    2626def __mkreports(reps): 
    2727    """Utility function used to create report data in same syntax as the 
     
    125125); 
    126126CREATE TABLE component ( 
    127127         name            text PRIMARY KEY, 
    128          owner           text 
     128         owner           text, 
     129         qaowner         text 
    129130); 
    130131CREATE TABLE milestone ( 
    131132         id              integer PRIMARY KEY, 
    132133         name            text, 
    133134         time            integer, 
     135         owner           text, 
    134136         descr           text, 
    135137         UNIQUE(name) 
    136138); 
     
    209211""", 
    210212""" 
    211213SELECT p.value AS __color__, 
    212    version AS __group__, 
    213    id AS ticket, summary, component, version, severity,  
     214   (CASE WHEN IFNULL(version, '') = '' THEN 'Not Specified' ELSE 'Version ' || version END) AS __group__, 
     215   id AS ticket, summary, component, milestone, severity,  
    214216   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner, 
    215217   time AS created, 
    216218   changetime AS _changetime, description AS _description, 
     
    218220  FROM ticket t, enum p 
    219221  WHERE status IN ('new', 'assigned', 'reopened')  
    220222AND p.name = t.priority AND p.type = 'priority' 
    221   ORDER BY (version IS NULL),version, p.value, severity, time 
     223  ORDER BY (IFNULL(version, '') = '') DESC,version, p.value, severity, time 
    222224"""), 
    223225#---------------------------------------------------------------------------- 
    224 ('All Tickets by Milestone', 
     226('Active Tickets by Milestone', 
    225227""" 
    226228This report shows how to color results by priority, 
    227229while grouping results by milestone. 
     
    231233""", 
    232234""" 
    233235SELECT p.value AS __color__, 
    234    milestone||' Release' AS __group__, 
     236   (CASE WHEN IFNULL(milestone, '') = '' THEN 'Not Assigned' ELSE milestone||' Release' END) AS __group__, 
    235237   id AS ticket, summary, component, version, severity,  
    236238   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner, 
    237239   time AS created, 
     
    240242  FROM ticket t, enum p 
    241243  WHERE status IN ('new', 'assigned', 'reopened')  
    242244AND p.name = t.priority AND p.type = 'priority' 
    243   ORDER BY (milestone IS NULL),milestone, p.value, severity, time 
     245  ORDER BY (IFNULL(milestone, '') = '') DESC,milestone, p.value, severity, time 
    244246"""), 
    245247#---------------------------------------------------------------------------- 
    246248('Assigned, Active Tickets by Owner', 
     
    248250List assigned tickets, group by ticket owner, sorted by priority. 
    249251""", 
    250252""" 
    251  
    252253SELECT p.value AS __color__, 
    253    owner AS __group__, 
    254    id AS ticket, summary, component, milestone, severity, time AS created, 
     254   (CASE WHEN IFNULL(owner, '') = '' THEN 'Not Assigned' ELSE owner END) AS __group__, 
     255   id AS ticket, summary, component, version, milestone, severity, time AS created, 
    255256   changetime AS _changetime, description AS _description, 
    256257   reporter AS _reporter 
    257258  FROM ticket t,enum p 
    258259  WHERE status = 'assigned' 
    259260AND p.name=t.priority AND p.type='priority' 
    260   ORDER BY owner, p.value, severity, time 
     261  ORDER BY (IFNULL(owner, '') = '') DESC, owner, p.value, severity, time 
    261262"""), 
    262263#---------------------------------------------------------------------------- 
    263264('Assigned, Active Tickets by Owner (Full Description)', 
     
    268269""" 
    269270SELECT p.value AS __color__, 
    270271   owner AS __group__, 
    271    id AS ticket, summary, component, milestone, severity, time AS created, 
     272   id AS ticket, summary, component, version, milestone, severity, time AS created, 
    272273   description AS _description_, 
    273274   changetime AS _changetime, reporter AS _reporter 
    274275  FROM ticket t, enum p 
     
    283284""", 
    284285""" 
    285286SELECT p.value AS __color__, 
    286    t.milestone AS __group__, 
     287   (CASE WHEN IFNULL(t.milestone, '') = '' THEN 'Not Assigned' ELSE t.milestone || ' Release' END) AS __group__, 
    287288   (CASE status  
    288289      WHEN 'closed' THEN 'color: #777; background: #ddd; border-color: #ccc;' 
    289290      ELSE  
     
    295296   time AS _time,reporter AS _reporter 
    296297  FROM ticket t,enum p 
    297298  WHERE p.name=t.priority AND p.type='priority' 
    298   ORDER BY (milestone IS NULL), milestone DESC, (status = 'closed'),  
     299  ORDER BY (IFNULL(milestone, '') = '') DESC, milestone DESC, (status = 'closed'),  
    299300        (CASE status WHEN 'closed' THEN modified ELSE -p.value END) DESC 
    300301"""), 
    301302#---------------------------------------------------------------------------- 
     
    308309""" 
    309310SELECT p.value AS __color__, 
    310311   (CASE status WHEN 'assigned' THEN 'Assigned' ELSE 'Owned' END) AS __group__, 
    311    id AS ticket, summary, component, version, milestone, 
     312   id AS ticket, summary, component, status, version, milestone, 
    312313   severity, priority, time AS created, 
    313314   changetime AS _changetime, description AS _description, 
    314315   reporter AS _reporter 
    315316  FROM ticket t, enum p 
    316   WHERE t.status IN ('new', 'assigned', 'reopened')  
     317  WHERE t.status <> 'closed'  
    317318AND p.name = t.priority AND p.type = 'priority' AND owner = '$USER' 
    318319  ORDER BY (status = 'assigned') DESC, p.value, milestone, severity, time 
    319320"""), 
     
    338339  WHERE status IN ('new', 'assigned', 'reopened')  
    339340AND p.name = t.priority AND p.type = 'priority' 
    340341  ORDER BY (owner = '$USER') DESC, p.value, milestone, severity, time 
     342"""), 
     343#---------------------------------------------------------------------------- 
     344('Open Tickets, Mine first', 
     345""" 
     346 * List all not closed tickets by priority. 
     347 * Show all tickets owned by the logged in user in a group first. 
     348""", 
     349""" 
     350SELECT p.value AS __color__, 
     351   (CASE owner  
     352     WHEN '$USER' THEN 'My Tickets'  
     353     ELSE 'Open Tickets'  
     354    END) AS __group__, 
     355   id AS ticket, summary, component, status, version, milestone, severity,  
     356   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner, 
     357   time AS created, 
     358   changetime AS _changetime, description AS _description, 
     359   reporter AS _reporter 
     360  FROM ticket t, enum p 
     361  WHERE status <> 'closed'  
     362AND p.name = t.priority AND p.type = 'priority' 
     363  ORDER BY (owner = '$USER') DESC, p.value, milestone, severity, time 
     364"""), 
     365#---------------------------------------------------------------------------- 
     366('Open Tickets by Version', 
     367""" 
     368 * List all not closed tickets by priority. 
     369 * Group results by version. 
     370""", 
     371""" 
     372SELECT p.value AS __color__, 
     373   (CASE WHEN IFNULL(version, '') = '' THEN 'Not Specified' ELSE 'Version ' || version END) AS __group__, 
     374   id AS ticket, summary, component, status, milestone, severity,  
     375   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner, 
     376   time AS created, 
     377   changetime AS _changetime, description AS _description, 
     378   reporter AS _reporter 
     379  FROM ticket t, enum p 
     380  WHERE status <> 'closed' 
     381AND p.name = t.priority AND p.type = 'priority' 
     382  ORDER BY (IFNULL(version, '') = '') desc,version, p.value, severity, time 
     383"""), 
     384#---------------------------------------------------------------------------- 
     385('Open Tickets by Milestone', 
     386""" 
     387 * List all not closed tickets by priority. 
     388 * Group results by milestone. 
     389""", 
     390""" 
     391SELECT p.value AS __color__, 
     392   (CASE WHEN IFNULL(milestone, '') = '' THEN 'Not Assigned' ELSE milestone||' Release' END) AS __group__, 
     393   id AS ticket, summary, component, status, version, severity,  
     394   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner, 
     395   time AS created, 
     396   changetime AS _changetime, description AS _description, 
     397   reporter AS _reporter 
     398  FROM ticket t, enum p 
     399  WHERE status <> 'closed'  
     400AND p.name = t.priority AND p.type = 'priority' 
     401  ORDER BY (IFNULL(milestone, '') = '') DESC,milestone, p.value, severity, time 
     402"""), 
     403#---------------------------------------------------------------------------- 
     404('Open Tickets by Owner', 
     405""" 
     406List not closed tickets, group by ticket owner, sorted by priority. 
     407""", 
     408""" 
     409SELECT p.value AS __color__, 
     410   (CASE WHEN IFNULL(owner, '') = '' THEN 'Not Assigned' ELSE owner END) AS __group__, 
     411   id AS ticket, summary, component, status, version, milestone, severity, 
     412   time AS created, changetime AS _changetime, description AS _description, 
     413   reporter AS _reporter 
     414  FROM ticket t,enum p 
     415  WHERE status <> 'closed' 
     416AND p.name=t.priority AND p.type='priority' 
     417  ORDER BY (IFNULL(owner, '') = '') DESC, owner, p.value, severity, time 
     418"""), 
     419#---------------------------------------------------------------------------- 
     420('Open Tickets by Status', 
     421""" 
     422 * List all not closed tickets by priority. 
     423 * Group results by status. 
     424""", 
     425""" 
     426SELECT p.value AS __color__, 
     427   status AS __group__, 
     428   id AS ticket, summary, component, version, milestone, severity, owner, 
     429   time AS created, 
     430   changetime AS _changetime, description AS _description, 
     431   reporter AS _reporter 
     432  FROM ticket t, enum q, enum p 
     433  WHERE status <> 'closed'  
     434AND q.name = t.status AND q.type = 'status' 
     435AND p.name = t.priority AND p.type = 'priority' 
     436  ORDER BY q.value, p.value, severity, time 
     437"""), 
     438#---------------------------------------------------------------------------- 
     439('Resolved Tickets, Mine first', 
     440""" 
     441 * List all resolved tickets by priority. 
     442 * Show all tickets owned by the logged in user in a group first. 
     443""", 
     444""" 
     445SELECT p.value AS __color__, 
     446   (CASE owner  
     447     WHEN '$USER' THEN 'My Tickets'  
     448     ELSE 'Active Tickets'  
     449    END) AS __group__, 
     450   id AS ticket, summary, component, version, milestone, severity,  
     451   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner, 
     452   time AS created, 
     453   changetime AS _changetime, description AS _description, 
     454   reporter AS _reporter 
     455  FROM ticket t, enum p 
     456  WHERE status = 'resolved'  
     457AND p.name = t.priority AND p.type = 'priority' 
     458  ORDER BY (owner = '$USER') DESC, p.value, milestone, severity, time 
     459"""), 
     460#---------------------------------------------------------------------------- 
     461('Resolved Tickets by Milestone', 
     462""" 
     463List resolved tickets, sorted by priority, grouped by milestone 
     464""", 
     465""" 
     466SELECT p.value AS __color__, 
     467   (CASE WHEN IFNULL(milestone, '') = '' THEN 'Not Assigned' ELSE milestone||' Release' END) AS __group__, 
     468   id AS ticket, summary, component, version, severity,  
     469   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner, 
     470   time AS created, 
     471   changetime AS _changetime, description AS _description, 
     472   reporter AS _reporter 
     473  FROM ticket t, enum p 
     474  WHERE status = 'resolved'  
     475AND p.name = t.priority AND p.type = 'priority' 
     476  ORDER BY (IFNULL(milestone, '') = '') DESC,milestone, p.value, severity, time 
     477"""), 
     478#---------------------------------------------------------------------------- 
     479('Resolved Tickets by Owner', 
     480""" 
     481List resolved tickets, group by ticket owner, sorted by priority. 
     482""", 
     483""" 
     484SELECT p.value AS __color__, 
     485   (CASE WHEN IFNULL(owner, '') = '' THEN 'Not Assigned' ELSE owner END) AS __group__, 
     486   id AS ticket, summary, component, version, milestone, severity, time AS created, 
     487   changetime AS _changetime, description AS _description, 
     488   reporter AS _reporter 
     489  FROM ticket t,enum p 
     490  WHERE status = 'resolved' 
     491AND p.name=t.priority AND p.type='priority' 
     492  ORDER BY (IFNULL(owner, '') = '') DESC, owner, p.value, severity, time 
     493"""), 
     494#---------------------------------------------------------------------------- 
     495('Completed Tickets by Milestone (Full Description)', 
     496""" 
     497Release Notes: List verified and closed tickets, group by milestone, include description. 
     498""", 
     499""" 
     500SELECT p.value AS __color__, 
     501   (CASE WHEN IFNULL(milestone, '') = '' THEN 'Not Assigned' ELSE milestone||' Release' END) AS __group__, 
     502   id AS ticket, summary, component, status, version, severity, time AS created, 
     503   description AS _description_, 
     504   changetime AS _changetime, reporter AS _reporter 
     505  FROM ticket t, enum p 
     506  WHERE status IN ('verified', 'closed') 
     507AND p.name = t.priority AND p.type = 'priority' 
     508  ORDER BY (IFNULL(milestone, '') = '') DESC,milestone, p.value, severity, time 
    341509""")) 
    342510 
    343511 
     
    347515 
    348516# (table, (column1, column2), ((row1col1, row1col2), (row2col1, row2col2))) 
    349517data = (('component', 
    350              ('name', 'owner'), 
    351                (('component1', 'somebody'), 
    352                 ('component2', 'somebody'))), 
     518             ('name', 'owner', 'qaowner'), 
     519               (('component1', 'somebody', 'qasomebody'), 
     520                ('component2', 'somebody', 'qasomebody'))), 
    353521           ('milestone', 
    354522             ('name', 'time'), 
    355523               (('', 0),  
     
    367535               (('status', 'new', 1), 
    368536                ('status', 'assigned', 2), 
    369537                ('status', 'reopened', 3), 
    370                 ('status', 'closed', 4), 
     538                ('status', 'resolved', 4), 
     539                ('status', 'verified', 5), 
     540                ('status', 'closed', 6), 
    371541                ('resolution', 'fixed', 1), 
    372542                ('resolution', 'invalid', 2), 
    373543                ('resolution', 'wontfix', 3), 
     
    426596  ('project', 'footer', 
    427597   ' Visit the Trac open source project at<br />' 
    428598   '<a href="http://trac.edgewall.com/">http://trac.edgewall.com/</a>'), 
     599  ('ticket', 'workflow', 'trac.workflows.QaRmtWorkflow'), 
    429600  ('ticket', 'default_version', ''), 
    430601  ('ticket', 'default_severity', 'normal'), 
    431602  ('ticket', 'default_priority', 'normal'), 
  • trac/Milestone.py

     
    6060    if not group: 
    6161        queries['all_tickets'] = env.href.query({'milestone': milestone}) 
    6262        queries['active_tickets'] = env.href.query({ 
    63             'milestone': milestone, 'status': ['new', 'assigned', 'reopened'] 
     63            'milestone': milestone, 'status': ['new', 'assigned', 'reopened', 'resolved'] 
    6464        }) 
    6565        queries['closed_tickets'] = env.href.query({ 
    66             'milestone': milestone, 'status': 'closed' 
     66            'milestone': milestone, 'status': ['closed', 'verified'] 
    6767        }) 
    6868    else: 
    6969        queries['all_tickets'] = env.href.query({ 
     
    7171        }) 
    7272        queries['active_tickets'] = env.href.query({ 
    7373            'milestone': milestone, grouped_by: group, 
    74             'status': ['new', 'assigned', 'reopened'] 
     74            'status': ['new', 'assigned', 'reopened', 'resolved'] 
    7575        }) 
    7676        queries['closed_tickets'] = env.href.query({ 
    7777            'milestone': milestone, grouped_by: group, 
    78             'status': 'closed' 
     78            'status': ['closed', 'verified'] 
    7979        }) 
    8080    return queries 
    8181 
    8282def calc_ticket_stats(tickets): 
    8383    total_cnt = len(tickets) 
    84     active = [ticket for ticket in tickets if ticket['status'] != 'closed'] 
     84    active = [ticket for ticket in tickets if ticket['status'] != 'closed' and ticket['status'] != 'verified'] 
    8585    active_cnt = len(active) 
    8686    closed_cnt = total_cnt - active_cnt 
    8787 
     
    116116                if datestr: 
    117117                    date = self.parse_date(datestr) 
    118118            descr = self.args.get('descr', '') 
     119            owner = self.args.get('owner', '') 
    119120            if not id: 
    120                 self.create_milestone(name, date, descr) 
     121                self.create_milestone(name, date, descr, owner) 
    121122            else: 
    122                 self.update_milestone(id, name, date, descr) 
     123                self.update_milestone(id, name, date, descr, owner) 
    123124        elif id: 
    124125            self.req.redirect(self.env.href.milestone(id)) 
    125126        else: 
     
    141142                            'Invalid Date Format') 
    142143        return seconds 
    143144 
    144     def create_milestone(self, name, date=0, descr=''): 
     145    def create_milestone(self, name, date=0, descr='', owner=''): 
    145146        self.perm.assert_permission(perm.MILESTONE_CREATE) 
    146147        if not name: 
    147148            raise TracError('You must provide a name for the milestone.', 
    148149                            'Required Field Missing') 
    149150        cursor = self.db.cursor() 
    150151        self.log.debug("Creating new milestone '%s'" % name) 
    151         cursor.execute("INSERT INTO milestone (id, name, time, descr) " 
    152                        "VALUES (NULL, %s, %d, %s)", name, date, descr) 
     152        cursor.execute("INSERT INTO milestone (id, name, time, descr, owner) " 
     153                       "VALUES (NULL, %s, %d, %s, %s)", name, date, descr, owner) 
    153154        self.db.commit() 
    154155        self.req.redirect(self.env.href.milestone(name)) 
    155156 
     
    178179        else: 
    179180            self.req.redirect(self.env.href.milestone(id)) 
    180181 
    181     def update_milestone(self, id, name, date, descr): 
     182    def update_milestone(self, id, name, date, descr, owner): 
    182183        self.perm.assert_permission(perm.MILESTONE_MODIFY) 
    183184        cursor = self.db.cursor() 
    184185        self.log.info("Updating milestone '%s'" % id) 
     
    188189            cursor.execute('UPDATE ticket SET milestone = %s ' 
    189190                            'WHERE milestone = %s', name, id) 
    190191            cursor.execute("UPDATE milestone SET name = %s, time = %d, " 
    191                            "descr = %s WHERE name = %s", 
    192                            name, date, descr, id) 
     192                           "descr = %s, owner = %s WHERE name = %s", 
     193                           name, date, descr, owner, id) 
    193194            self.db.commit() 
    194195            self.req.redirect(self.env.href.milestone(name)) 
    195196        else: 
     
    222223 
    223224    def get_milestone(self, name): 
    224225        cursor = self.db.cursor() 
    225         cursor.execute("SELECT name, time, descr FROM milestone " 
     226        cursor.execute("SELECT name, time, descr, owner FROM milestone " 
    226227                       "WHERE name = %s ORDER BY time, name", name) 
    227228        row = cursor.fetchone() 
    228229        cursor.close() 
     
    237238        t = row['time'] and int(row['time']) 
    238239        if t > 0: 
    239240            milestone['date'] = time.strftime('%x', time.localtime(t)) 
     241        milestone['owner'] = row['owner'] or '' 
    240242        return milestone 
    241243 
    242244    def render(self): 
  • trac/Timeline.py

     
    5252        REOPENED_TICKET = 4 
    5353        WIKI = 5 
    5454        MILESTONE = 6 
     55        VERIFIED_TICKET = 7 
     56        RESOLVED_TICKET = 8 
     57        RETESTED_TICKET = 9 
    5558 
    5659        q = [] 
    5760        if changeset: 
     
    6063                     "FROM revision WHERE time>=%s AND time<=%s" % 
    6164                     (start, stop)) 
    6265        if tickets: 
     66            # New tickets 
    6367            q.append("SELECT time, id AS idata, '' AS tdata, 2 AS type, " 
    6468                     "summary AS message, reporter AS author " 
    6569                     "FROM ticket WHERE time>=%s AND time<=%s" % 
    6670                     (start, stop)) 
    67             q.append("SELECT time, ticket AS idata, '' AS tdata, 4 AS type, " 
    68                      "'' AS message, author " 
    69                      "FROM ticket_change WHERE field='status' " 
    70                      "AND newvalue='reopened' AND time>=%s AND time<=%s" % 
    71                      (start, stop)) 
     71            # Reopened tickets 
    7272            q.append("SELECT t1.time AS time, t1.ticket AS idata," 
     73                     "       '' AS tdata, 4 AS type," 
     74                     "       t3.newvalue AS message, t1.author AS author" 
     75                     " FROM ticket_change t1" 
     76                     "   LEFT OUTER JOIN ticket_change t3 ON t1.time = t3.time" 
     77                     "     AND t1.ticket = t3.ticket AND t3.field = 'comment'" 
     78                     " WHERE t1.field = 'status' AND t1.newvalue = 'reopened'" 
     79                     "   AND t1.time >= %s AND t1.time <= %s" % (start,stop)) 
     80            # Closed tickets (including resolution field for old workflow) 
     81            q.append("SELECT t1.time AS time, t1.ticket AS idata," 
    7382                     "       t2.newvalue AS tdata, 3 AS type," 
    7483                     "       t3.newvalue AS message, t1.author AS author" 
    7584                     " FROM ticket_change t1" 
    76                      "   INNER JOIN ticket_change t2 ON t1.ticket = t2.ticket" 
    77                      "     AND t1.time = t2.time" 
     85                     "   LEFT OUTER JOIN ticket_change t2 ON t1.ticket = t2.ticket" 
     86                     "     AND t1.time = t2.time AND t2.field = 'resolution'" 
    7887                     "   LEFT OUTER JOIN ticket_change t3 ON t1.time = t3.time" 
    7988                     "     AND t1.ticket = t3.ticket AND t3.field = 'comment'" 
    8089                     " WHERE t1.field = 'status' AND t1.newvalue = 'closed'" 
    81                      "   AND t2.field = 'resolution'" 
    8290                     "   AND t1.time >= %s AND t1.time <= %s" % (start,stop)) 
     91            # Verified tickets (including resolution field for customized workflows) 
     92            q.append("SELECT t1.time AS time, t1.ticket AS idata," 
     93                     "       t2.newvalue AS tdata, 7 AS type," 
     94                     "       t3.newvalue AS message, t1.author AS author" 
     95                     " FROM ticket_change t1" 
     96                     "   LEFT OUTER JOIN ticket_change t2 ON t1.ticket = t2.ticket" 
     97                     "     AND t1.time = t2.time AND t2.field = 'resolution'" 
     98                     "   LEFT OUTER JOIN ticket_change t3 ON t1.time = t3.time" 
     99                     "     AND t1.ticket = t3.ticket AND t3.field = 'comment'" 
     100                     " WHERE t1.field = 'status' AND t1.newvalue = 'verified'" 
     101                     "   AND t1.oldvalue<>'closed'" 
     102                     "   AND t1.time >= %s AND t1.time <= %s" % (start,stop)) 
     103            # Resolved tickets (including resolution field) 
     104            q.append("SELECT t1.time AS time, t1.ticket AS idata," 
     105                     "       t2.newvalue AS tdata, 8 AS type," 
     106                     "       t3.newvalue AS message, t1.author AS author" 
     107                     " FROM ticket_change t1" 
     108                     "   LEFT OUTER JOIN ticket_change t2 ON t1.ticket = t2.ticket" 
     109                     "     AND t1.time = t2.time AND t2.field = 'resolution'" 
     110                     "   LEFT OUTER JOIN ticket_change t3 ON t1.time = t3.time" 
     111                     "     AND t1.ticket = t3.ticket AND t3.field = 'comment'" 
     112                     " WHERE t1.field = 'status' AND t1.newvalue = 'resolved'" 
     113                     "   AND t1.oldvalue NOT IN ('verified', 'closed')" 
     114                     "   AND t1.time >= %s AND t1.time <= %s" % (start,stop)) 
     115            # Retested tickets 
     116            q.append("SELECT t1.time AS time, t1.ticket AS idata," 
     117                     "       '' AS tdata, 9 AS type," 
     118                     "       t3.newvalue AS message, t1.author AS author" 
     119                     " FROM ticket_change t1" 
     120                     "   LEFT OUTER JOIN ticket_change t3 ON t1.time = t3.time" 
     121                     "     AND t1.ticket = t3.ticket AND t3.field = 'comment'" 
     122                     " WHERE t1.field = 'status' AND t1.newvalue = 'resolved'" 
     123                     "   AND t1.oldvalue IN ('verified', 'closed')" 
     124                     "   AND t1.time >= %s AND t1.time <= %s" % (start,stop)) 
    83125        if wiki: 
    84126            q.append("SELECT time, -1 AS idata, name AS tdata, 5 AS type, " 
    85127                     "comment AS message, author " 
     
    87129                     (start, stop)) 
    88130        if milestone: 
    89131            q.append("SELECT time, -1 AS idata, '' AS tdata, 6 AS type, " 
    90                      "name AS message, '' AS author "  
     132                     "name AS message, owner AS author "  
    91133                     "FROM milestone WHERE time>=%s AND time<=%s" % 
    92134                     (start, stop)) 
    93135 
     
    110152                    'date': time.strftime('%x', t), 
    111153                    'datetime': time.strftime('%a, %d %b %Y %H:%M:%S GMT', gmt), 
    112154                    'idata': int(row['idata']), 
    113                     'tdata': row['tdata'], 
     155                    'tdata': row['tdata'] or '', 
    114156                    'type': int(row['type']), 
    115157                    'message': row['message'] or '', 
    116158                    'author': util.escape(row['author'] or 'anonymous') 
  • trac/workflows/Base.py

     
     1# -*- coding: iso8859-1 -*- 
     2# 
     3# Copyright (C) 2003, 2004 Edgewall Software 
     4# Copyright (C) 2003, 2004 Jonas Borgström <jonas@edgewall.com> 
     5# 
     6# Trac is free software; you can redistribute it and/or 
     7# modify it under the terms of the GNU General Public License as 
     8# published by the Free Software Foundation; either version 2 of the 
     9# License, or (at your option) any later version. 
     10# 
     11# Trac is distributed in the hope that it will be useful, 
     12# but WITHOUT ANY WARRANTY; without even the implied warranty of 
     13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
     14# General Public License for more details. 
     15# 
     16# You should have received a copy of the GNU General Public License 
     17# along with this program; if not, write to the Free Software 
     18# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 
     19# 
     20# Author: Pavel Kourochka <pkou@ua.fm> 
     21# 
     22# Abstract workflow definition 
     23 
     24class WorkflowBase: 
     25    """ 
     26    Generic workflow class for Trac. 
     27    """ 
     28 
     29    def __init__(self, env, db, user): 
     30        """ 
     31        Constructor for workflow class. 
     32        """ 
     33        self.env = env 
     34        self.db = db 
     35        self.user = user 
     36 
     37    def get_actions(self, ticket): 
     38        """ 
     39        For existing tickets only. 
     40        Return the list of available actions for specified ticket. 
     41        """ 
     42        raise Exception, "WorkflowBase::get_actions not implemented" 
     43 
     44    def do_action(self, ticket, action, args): 
     45        """ 
     46        For new and existing tickets. 
     47        Perform action on a ticket.  For new tickets, action name is 'create'. 
     48        """ 
     49        raise Exception, "WorkflowBase::do_action not implemented" 
     50 
     51    def get_actions_template(self, ticket): 
     52        """ 
     53        For new and existing tickets. 
     54        Return the name of ClearSilver template file for the workflow. 
     55        Return None if no additional template is required. 
     56        """ 
     57        return None 
     58 
     59    def init_template(self, ticket, hdf): 
     60        """ 
     61        For new and existing tickets. 
     62        Initialize ClearSilver variables for actions template. 
     63        Called if get_actions_template() returns file name only. 
     64        """ 
     65        pass 
     66 
     67    def validate(self, ticket): 
     68        """ 
     69        For new and existing tickets. 
     70        Validate ticket. 
     71        Return list of Wiki strings that describe errors in the ticket. 
     72        """ 
     73        return [] 
     74 
     75    def on_insert(self, ticket): 
     76        """ 
     77        For new tickets only. 
     78        Update ticket fields just before inserting the ticket into database. 
     79        """ 
     80        pass 
     81 
     82    def on_update(self, ticket): 
     83        """ 
     84        For existing tickets only. 
     85        Update ticket fields just before saving the ticket into database. 
     86        """ 
     87        pass 
  • trac/workflows/QaRmtWorkflow.py

     
     1# -*- coding: iso8859-1 -*- 
     2# 
     3# Copyright (C) 2003, 2004 Edgewall Software 
     4# Copyright (C) 2003, 2004 Jonas Borgström <jonas@edgewall.com> 
     5# 
     6# Trac is free software; you can redistribute it and/or 
     7# modify it under the terms of the GNU General Public License as 
     8# published by the Free Software Foundation; either version 2 of the 
     9# License, or (at your option) any later version. 
     10# 
     11# Trac is distributed in the hope that it will be useful, 
     12# but WITHOUT ANY WARRANTY; without even the implied warranty of 
     13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
     14# General Public License for more details. 
     15# 
     16# You should have received a copy of the GNU General Public License 
     17# along with this program; if not, write to the Free Software 
     18# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 
     19# 
     20# Author: Pavel Kourochka <pkou@ua.fm> 
     21# 
     22# Workflow definition for development, QA, and release management teams 
     23 
     24from trac.workflows.SimpleWorkflow import SimpleWorkflow 
     25 
     26class QaRmtWorkflow(SimpleWorkflow): 
     27 
     28    def get_actions(self, ticket): 
     29        actions = { 
     30 'new':      ['leave','reassign','resolve',                          'accept'], 
     31 'assigned': ['leave','reassign','resolve',                                  ], 
     32 'reopened': ['leave','reassign','resolve',                                  ], 
     33 'resolved': ['leave','reassign',          'reopen',         'close','verify'], 
     34 'verified': ['leave','reassign',          'reopen','retest','close'         ], 
     35 'closed':   ['leave',                     'reopen','retest'                 ] 
     36        } 
     37        return actions.get(ticket['status'], ['leave']) 
     38 
     39    def do_action(self, ticket, action, args): 
     40        if action == 'accept': 
     41            ticket['status'] = 'assigned' 
     42            ticket['owner'] = self.user 
     43        elif action == 'resolve': 
     44            ticket['status'] = 'resolved' 
     45            ticket['resolution'] = args.get('resolve_resolution') 
     46            ticket['owner'] = '' 
     47        elif action == 'verify': 
     48            ticket['status'] = 'verified' 
     49            ticket['owner'] = '' 
     50        elif action == 'close': 
     51            ticket['status'] = 'closed' 
     52        elif action == 'reassign': 
     53            newowner = args.get('reassign_owner') 
     54            if ticket['owner'] != newowner: 
     55                if ticket['status'] == 'assigned': ticket['status'] = 'new' 
     56                ticket['owner'] = newowner 
     57        elif action == 'reopen': 
     58            ticket['status'] = 'reopened' 
     59            ticket['resolution'] = '' 
     60            ticket['owner'] = '' 
     61        elif action == 'retest': 
     62            ticket['status'] = 'resolved' 
     63            ticket['owner'] = '' 
     64 
     65    def get_actions_template(self, ticket): 
     66        if ticket.has_key('id'): 
     67            return 'ticket_workflow_qarmt.cs' 
     68        else: 
     69            return None 
     70 
     71    def on_insert(self, ticket): 
     72        SimpleWorkflow.on_insert(self, ticket) 
     73 
     74        # The owner field defaults to the milestone owner if 
     75        # the component does not have any owner 
     76        cursor = self.db.cursor() 
     77        if ticket.get('owner', '') == '': 
     78            cursor.execute('SELECT owner FROM milestone ' 
     79                           'WHERE name=%s', ticket.get('milestone', '')) 
     80            ticket['owner'] = cursor.fetchone()[0] or '' 
     81 
     82    def on_update(self, ticket): 
     83        SimpleWorkflow.on_update(self, ticket) 
     84        if not ticket._old: return # Not modified 
     85 
     86        cursor = self.db.cursor() 
     87        status = ticket.get('status', 'new') 
     88        component = ticket.get('component', '') 
     89        milestone = ticket.get('milestone', '') 
     90 
     91        # If the milestone is changed on a 'new' ticket then owner field 
     92        # is updated accordingly if the component does not have any owner. 
     93        # (related to #623). 
     94        if status == 'new' and ticket._old.has_key('milestone') and \ 
     95               not ticket._old.has_key('component') and \ 
     96               not ticket._old.has_key('owner'): 
     97            cursor.execute('SELECT owner FROM component ' 
     98                           'WHERE name=%s', component) 
     99            if not cursor.fetchone()[0]: 
     100                cursor.execute('SELECT owner FROM milestone ' 
     101                               'WHERE name=%s', ticket._old['milestone']) 
     102                old_owner = cursor.fetchone()[0] 
     103                if ticket['owner'] == old_owner: 
     104                    cursor.execute('SELECT owner FROM milestone ' 
     105                                   'WHERE name=%s', milestone) 
     106                    ticket['owner'] = cursor.fetchone()[0] or '' 
     107 
     108        # 1. The owner field defaults to the component owner for active tickets 
     109        if ticket.get('owner', '') == '' and status in ['new', 'reopened']: 
     110            cursor.execute('SELECT owner FROM component ' 
     111                           'WHERE name=%s', component) 
     112            newowner = cursor.fetchone()[0] 
     113            if newowner: ticket['owner'] = newowner 
     114 
     115        # 2. The owner field defaults to component QA owner for testing tickets 
     116        if ticket.get('owner', '') == '' and status == 'resolved': 
     117            cursor.execute('SELECT qaowner FROM component ' 
     118                           'WHERE name=%s', component) 
     119            newowner = cursor.fetchone()[0] 
     120            if newowner: ticket['owner'] = newowner 
     121 
     122        # 3. The owner field defaults to milestone owner for open tickets 
     123        if ticket.get('owner', '') == '' and status != 'closed': 
     124            cursor.execute('SELECT owner FROM milestone ' 
     125                           'WHERE name=%s', milestone) 
     126            newowner = cursor.fetchone()[0] 
     127            if newowner: ticket['owner'] = newowner 
     128 
     129        # 4. The owner field defaults to reporter for verified tickets 
     130        if ticket.get('owner', '') == '' and status == 'verified': 
     131            reporter = ticket.get('reporter', '') 
     132            if reporter: ticket['owner'] = reporter 
  • trac/workflows/__init__.py

     
     1__all__ = ['Base', 'SimpleWorkflow', 'QaRmtWorkflow'] 
  • trac/workflows/SimpleWorkflow.py

     
     1# -*- coding: iso8859-1 -*- 
     2# 
     3# Copyright (C) 2003, 2004 Edgewall Software 
     4# Copyright (C) 2003, 2004 Jonas Borgström <jonas@edgewall.com> 
     5# 
     6# Trac is free software; you can redistribute it and/or 
     7# modify it under the terms of the GNU General Public License as 
     8# published by the Free Software Foundation; either version 2 of the 
     9# License, or (at your option) any later version. 
     10# 
     11# Trac is distributed in the hope that it will be useful, 
     12# but WITHOUT ANY WARRANTY; without even the implied warranty of 
     13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
     14# General Public License for more details. 
     15# 
     16# You should have received a copy of the GNU General Public License 
     17# along with this program; if not, write to the Free Software 
     18# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 
     19# 
     20# Author: Pavel Kourochka <pkou@ua.fm> 
     21# 
     22# Simple workflow definition (as in Trac 0.8) 
     23 
     24from trac.workflows.Base import WorkflowBase 
     25 
     26class SimpleWorkflow(WorkflowBase): 
     27 
     28    def get_actions(self, ticket): 
     29        actions = { 
     30            'new':      ['leave', 'resolve', 'reassign', 'accept'], 
     31            'assigned': ['leave', 'resolve', 'reassign'          ], 
     32            'reopened': ['leave', 'resolve', 'reassign'          ], 
     33            'closed':   ['leave',                        'reopen'] 
     34        } 
     35        return actions.get(ticket['status'], ['leave']) 
     36 
     37    def do_action(self, ticket, action, args): 
     38        if action == 'accept': 
     39            ticket['status'] = 'assigned' 
     40            ticket['owner'] = self.user 
     41        elif action == 'resolve': 
     42            ticket['status'] = 'closed' 
     43            ticket['resolution'] = args.get('resolve_resolution') 
     44        elif action == 'reassign': 
     45            ticket['owner'] = args.get('reassign_owner') 
     46            ticket['status'] = 'new' 
     47        elif action == 'reopen': 
     48            ticket['status'] = 'reopened' 
     49            ticket['resolution'] = '' 
     50 
     51    def get_actions_template(self, ticket): 
     52        if ticket.has_key('id'): 
     53            return 'ticket_workflow_simple.cs' 
     54        else: 
     55            return None 
     56 
     57    def init_template(self, ticket, hdf): 
     58        WorkflowBase.init_template(self, ticket, hdf) 
     59        if ticket.has_key('id'): 
     60            for a in self.get_actions(ticket): 
     61                hdf.setValue('ticket.workflow.action.' + a, '1') 
     62 
     63    def validate(self, ticket): 
     64        err = WorkflowBase.validate(self, ticket) 
     65        if not ticket.get('summary'): 
     66            err.append("The ticket must contain '''Summary''' field.") 
     67        return err 
     68 
     69    def on_insert(self, ticket): 
     70        WorkflowBase.on_insert(self, ticket) 
     71 
     72        # The owner field defaults to the component owner 
     73        cursor = self.db.cursor() 
     74        if ticket.get('owner', '') == '': 
     75            cursor.execute('SELECT owner FROM component ' 
     76                           'WHERE name=%s', ticket.get('component', '')) 
     77            ticket['owner'] = cursor.fetchone()[0] or '' 
     78 
     79    def on_update(self, ticket): 
     80        WorkflowBase.on_update(self, ticket) 
     81        if not ticket._old: return # Not modified 
     82 
     83        # If the component is changed on a 'new' ticket then owner field 
     84        # is updated accordingly. (#623). 
     85        cursor = self.db.cursor() 
     86        if ticket['status'] == 'new' and ticket._old.has_key('component') and \ 
     87               not ticket._old.has_key('owner'): 
     88            cursor.execute('SELECT owner FROM component ' 
     89                           'WHERE name=%s', ticket._old['component']) 
     90            old_owner = cursor.fetchone()[0] 
     91            if ticket['owner'] == old_owner: 
     92                cursor.execute('SELECT owner FROM component ' 
     93                               'WHERE name=%s', ticket['component']) 
     94                ticket['owner'] = cursor.fetchone()[0] or '' 
  • trac/Roadmap.py

     
    4848            icalhref += '&show=all' 
    4949            self.req.hdf.setValue('roadmap.href.list', 
    5050                                   self.env.href.roadmap()) 
    51             query = "SELECT name, time, descr FROM milestone " \ 
     51            query = "SELECT name, time, descr, owner FROM milestone " \ 
    5252                    "WHERE name != '' " \ 
    5353                    "ORDER BY (IFNULL(time, 0) = 0) ASC, time ASC, name" 
    5454        else: 
    5555            self.req.hdf.setValue('roadmap.showall', '1') 
    5656            self.req.hdf.setValue('roadmap.href.list', 
    5757                                   self.env.href.roadmap('all')) 
    58             query = "SELECT name, time, descr FROM milestone " \ 
     58            query = "SELECT name, time, descr, owner FROM milestone " \ 
    5959                    "WHERE name != '' " \ 
    6060                    "AND (time IS NULL OR time = 0 OR time > %d) " \ 
    6161                    "ORDER BY (IFNULL(time, 0) = 0) ASC, time ASC, name" % time() 
     
    7474            milestone = { 
    7575                'name': row['name'], 
    7676                'href': self.env.href.milestone(row['name']), 
     77                'owner': row['owner'] or '', 
    7778                'time': row['time'] and int(row['time']) 
    7879            } 
    7980            descr = row['descr'] 
     
    113114            status = ticket['status'] 
    114115            if status == 'new' or status == 'reopened' and not ticket['owner']: 
    115116                return 'NEEDS-ACTION' 
    116             elif status == 'assigned' or status == 'reopened': 
     117            elif status != 'closed': 
    117118                return 'IN-PROCESS' 
    118119            elif status == 'closed': 
    119120                if ticket['resolution'] == 'fixed': return 'COMPLETED' 
  • trac/upgrades/db8.py

     
     1sql = """ 
     2-- Add statuses 'resolved' and 'verified' 
     3UPDATE enum SET value = 6 WHERE type = 'status' AND name = 'closed'; 
     4INSERT INTO enum (type, name, value) VALUES ('status', 'resolved', 4); 
     5INSERT INTO enum (type, name, value) VALUES ('status', 'verified', 5); 
     6 
     7-- Add QA Contact to 'component' 
     8CREATE TEMPORARY TABLE component_backup AS SELECT * FROM component; 
     9DROP TABLE component; 
     10CREATE TABLE component ( 
     11         name            text PRIMARY KEY, 
     12         owner           text, 
     13         qaowner         text 
     14); 
     15INSERT INTO component SELECT name, owner, owner AS qaowner FROM component_backup; 
     16DROP TABLE component_backup; 
     17 
     18-- Add Release Manager Contact to 'milestone' 
     19CREATE TEMPORARY TABLE milestone_backup AS SELECT * FROM milestone; 
     20DROP TABLE milestone; 
     21CREATE TABLE milestone ( 
     22         id              integer PRIMARY KEY, 
     23         name            text, 
     24         time            integer, 
     25         owner           text, 
     26         descr           text, 
     27         UNIQUE(name) 
     28); 
     29INSERT INTO milestone SELECT id, name, time, '' AS owner, descr FROM milestone_backup; 
     30DROP TABLE milestone_backup; 
     31 
     32-- Modify 'All Tickets by Version' report 
     33UPDATE report SET sql = ' 
     34SELECT p.value AS __color__, 
     35   (CASE WHEN IFNULL(version, '''') = '''' THEN ''Not Specified'' ELSE ''Version '' || version END) AS __group__, 
     36   id AS ticket, summary, component, milestone, severity,  
     37   (CASE status WHEN ''assigned'' THEN owner||'' *'' ELSE owner END) AS owner, 
     38   time AS created, 
     39   changetime AS _changetime, description AS _description, 
     40   reporter AS _reporter 
     41  FROM ticket t, enum p 
     42  WHERE status IN (''new'', ''assigned'', ''reopened'')  
     43AND p.name = t.priority AND p.type = ''priority'' 
     44  ORDER BY (IFNULL(version, '''') = '''') DESC,version, p.value, severity, time 
     45' WHERE title = 'Active Tickets by Version'; 
     46 
     47-- Modify 'All Tickets by Milestone' report 
     48UPDATE report SET sql = ' 
     49SELECT p.value AS __color__, 
     50   (CASE WHEN IFNULL(milestone, '''') = '''' THEN ''Not Assigned'' ELSE milestone||'' Release'' END) AS __group__, 
     51   id AS ticket, summary, component, version, severity,  
     52   (CASE status WHEN ''assigned'' THEN owner||'' *'' ELSE owner END) AS owner, 
     53   time AS created, 
     54   changetime AS _changetime, description AS _description, 
     55   reporter AS _reporter 
     56  FROM ticket t, enum p 
     57  WHERE status IN (''new'', ''assigned'', ''reopened'')  
     58AND p.name = t.priority AND p.type = ''priority'' 
     59  ORDER BY (IFNULL(milestone, '''') = '''') DESC,milestone, p.value, severity, time 
     60', title = 'Active Tickets by Milestone' 
     61WHERE title = 'All Tickets by Milestone'; 
     62 
     63-- Modify 'Assigned, Active Tickets by Owner' report 
     64UPDATE report SET sql = ' 
     65SELECT p.value AS __color__, 
     66   (CASE WHEN IFNULL(owner, '''') = '''' THEN ''Not Assigned'' ELSE owner END) AS __group__, 
     67   id AS ticket, summary, component, version, milestone, severity, time AS created, 
     68   changetime AS _changetime, description AS _description, 
     69   reporter AS _reporter 
     70  FROM ticket t,enum p 
     71  WHERE status = ''assigned'' 
     72AND p.name=t.priority AND p.type=''priority'' 
     73  ORDER BY (IFNULL(owner, '''') = '''') DESC, owner, p.value, severity, time 
     74' WHERE title = 'Assigned, Active Tickets by Owner'; 
     75 
     76-- Modify 'Assigned, Active Tickets by Owner (Full Description)' report 
     77UPDATE report SET sql = ' 
     78SELECT p.value AS __color__, 
     79   owner AS __group__, 
     80   id AS ticket, summary, component, version, milestone, severity, time AS created, 
     81   description AS _description_, 
     82   changetime AS _changetime, reporter AS _reporter 
     83  FROM ticket t, enum p 
     84  WHERE status = ''assigned'' 
     85AND p.name = t.priority AND p.type = ''priority'' 
     86  ORDER BY owner, p.value, severity, time 
     87' WHERE title = 'Assigned, Active Tickets by Owner (Full Description)'; 
     88 
     89-- Modify 'All Tickets By Milestone  (Including closed)' report 
     90UPDATE report SET sql = ' 
     91SELECT p.value AS __color__, 
     92   (CASE WHEN IFNULL(t.milestone, '''') = '''' THEN ''Not Assigned'' ELSE t.milestone || '' Release'' END) AS __group__, 
     93   (CASE status  
     94      WHEN ''closed'' THEN ''color: #777; background: #ddd; border-color: #ccc;'' 
     95      ELSE  
     96        (CASE owner WHEN ''$USER'' THEN ''font-weight: bold'' END) 
     97    END) AS __style__, 
     98   id AS ticket, summary, component, status,  
     99   resolution,version, severity, priority, owner, 
     100   changetime AS modified, 
     101   time AS _time,reporter AS _reporter 
     102  FROM ticket t,enum p 
     103  WHERE p.name=t.priority AND p.type=''priority'' 
     104  ORDER BY (IFNULL(milestone, '''') = '''') DESC, milestone DESC, (status = ''closed''),  
     105        (CASE status WHEN ''closed'' THEN modified ELSE -p.value END) DESC 
     106' WHERE title = 'All Tickets By Milestone  (Including closed)'; 
     107 
     108-- Modify 'My Tickets' report 
     109UPDATE report SET sql = ' 
     110SELECT p.value AS __color__, 
     111   (CASE status WHEN ''assigned'' THEN ''Assigned'' ELSE ''Owned'' END) AS __group__, 
     112   id AS ticket, summary, component, status, version, milestone, 
     113   severity, priority, time AS created, 
     114   changetime AS _changetime, description AS _description, 
     115   reporter AS _reporter 
     116  FROM ticket t, enum p 
     117  WHERE t.status <> ''closed''  
     118AND p.name = t.priority AND p.type = ''priority'' AND owner = ''$USER'' 
     119  ORDER BY (status = ''assigned'') DESC, p.value, milestone, severity, time 
     120' WHERE title = 'My Tickets'; 
     121 
     122-- New reports 
     123 
     124INSERT INTO report VALUES(NULL,NULL,'Open Tickets, Mine first',' 
     125SELECT p.value AS __color__, 
     126   (CASE owner  
     127     WHEN ''$USER'' THEN ''My Tickets''  
     128     ELSE ''Open Tickets''  
     129    END) AS __group__, 
     130   id AS ticket, summary, component, status, version, milestone, severity,  
     131   (CASE status WHEN ''assigned'' THEN owner||'' *'' ELSE owner END) AS owner, 
     132   time AS created, 
     133   changetime AS _changetime, description AS _description, 
     134   reporter AS _reporter 
     135  FROM ticket t, enum p 
     136  WHERE status <> ''closed''  
     137AND p.name = t.priority AND p.type = ''priority'' 
     138  ORDER BY (owner = ''$USER'') DESC, p.value, milestone, severity, time 
     139',' 
     140 * List all not closed tickets by priority. 
     141 * Show all tickets owned by the logged in user in a group first. 
     142'); 
     143 
     144INSERT INTO report VALUES(NULL,NULL,'Open Tickets by Version',' 
     145SELECT p.value AS __color__, 
     146   (CASE WHEN IFNULL(version, '''') = '''' THEN ''Not Specified'' ELSE ''Version '' || version END) AS __group__, 
     147   id AS ticket, summary, component, status, milestone, severity,  
     148   (CASE status WHEN ''assigned'' THEN owner||'' *'' ELSE owner END) AS owner, 
     149   time AS created, 
     150   changetime AS _changetime, description AS _description, 
     151   reporter AS _reporter 
     152  FROM ticket t, enum p 
     153  WHERE status <> ''closed'' 
     154AND p.name = t.priority AND p.type = ''priority'' 
     155  ORDER BY (IFNULL(version, '''') = '''') desc,version, p.value, severity, time 
     156',' 
     157 * List all not closed tickets by priority. 
     158 * Group results by version. 
     159'); 
     160 
     161INSERT INTO report VALUES(NULL,NULL,'Open Tickets by Milestone',' 
     162SELECT p.value AS __color__, 
     163   (CASE WHEN IFNULL(milestone, '''') = '''' THEN ''Not Assigned'' ELSE milestone||'' Release'' END) AS __group__, 
     164   id AS ticket, summary, component, status, version, severity,  
     165   (CASE status WHEN ''assigned'' THEN owner||'' *'' ELSE owner END) AS owner, 
     166   time AS created, 
     167   changetime AS _changetime, description AS _description, 
     168   reporter AS _reporter 
     169  FROM ticket t, enum p 
     170  WHERE status <> ''closed''  
     171AND p.name = t.priority AND p.type = ''priority'' 
     172  ORDER BY (IFNULL(milestone, '''') = '''') DESC,milestone, p.value, severity, time 
     173',' 
     174 * List all not closed tickets by priority. 
     175 * Group results by milestone. 
     176'); 
     177 
     178INSERT INTO report VALUES(NULL,NULL,'Open Tickets by Owner',' 
     179SELECT p.value AS __color__, 
     180   (CASE WHEN IFNULL(owner, '''') = '''' THEN ''Not Assigned'' ELSE owner END) AS __group__, 
     181   id AS ticket, summary, component, status, version, milestone, severity, 
     182   time AS created, changetime AS _changetime, description AS _description, 
     183   reporter AS _reporter 
     184  FROM ticket t,enum p 
     185  WHERE status <> ''closed'' 
     186AND p.name=t.priority AND p.type=''priority'' 
     187  ORDER BY (IFNULL(owner, '''') = '''') DESC, owner, p.value, severity, time 
     188',' 
     189List not closed tickets, group by ticket owner, sorted by priority. 
     190'); 
     191 
     192INSERT INTO report VALUES(NULL,NULL,'Open Tickets by Status',' 
     193SELECT p.value AS __color__, 
     194   status AS __group__, 
     195   id AS ticket, summary, component, version, milestone, severity, owner, 
     196   time AS created, 
     197   changetime AS _changetime, description AS _description, 
     198   reporter AS _reporter 
     199  FROM ticket t, enum q, enum p 
     200  WHERE status <> ''closed''  
     201AND q.name = t.status AND q.type = ''status'' 
     202AND p.name = t.priority AND p.type = ''priority'' 
     203  ORDER BY q.value, p.value, severity, time 
     204',' 
     205 * List all not closed tickets by priority. 
     206 * Group results by status. 
     207'); 
     208 
     209INSERT INTO report VALUES(NULL,NULL,'Resolved Tickets, Mine first',' 
     210SELECT p.value AS __color__, 
     211   (CASE owner  
     212     WHEN ''$USER'' THEN ''My Tickets''  
     213     ELSE ''Active Tickets''  
     214    END) AS __group__, 
     215   id AS ticket, summary, component, version, milestone, severity,  
     216   (CASE status WHEN ''assigned'' THEN owner||'' *'' ELSE owner END) AS owner, 
     217   time AS created, 
     218   changetime AS _changetime, description AS _description, 
     219   reporter AS _reporter 
     220  FROM ticket t, enum p 
     221  WHERE status = ''resolved''  
     222AND p.name = t.priority AND p.type = ''priority'' 
     223  ORDER BY (owner = ''$USER'') DESC, p.value, milestone, severity, time 
     224',' 
     225 * List all resolved tickets by priority. 
     226 * Show all tickets owned by the logged in user in a group first. 
     227'); 
     228 
     229INSERT INTO report VALUES(NULL,NULL,'Resolved Tickets by Milestone',' 
     230SELECT p.value AS __color__, 
     231   (CASE WHEN IFNULL(milestone, '''') = '''' THEN ''Not Assigned'' ELSE milestone||'' Release'' END) AS __group__, 
     232   id AS ticket, summary, component, version, severity,  
     233   (CASE status WHEN ''assigned'' THEN owner||'' *'' ELSE owner END) AS owner, 
     234   time AS created, 
     235   changetime AS _changetime, description AS _description, 
     236   reporter AS _reporter 
     237  FROM ticket t, enum p 
     238  WHERE status = ''resolved''  
     239AND p.name = t.priority AND p.type = ''priority'' 
     240  ORDER BY (IFNULL(milestone, '''') = '''') DESC,milestone, p.value, severity, time 
     241',' 
     242List resolved tickets, sorted by priority, grouped by milestone 
     243'); 
     244 
     245INSERT INTO report VALUES(NULL,NULL,'Resolved Tickets by Owner',' 
     246SELECT p.value AS __color__, 
     247   (CASE WHEN IFNULL(owner, '''') = '''' THEN ''Not Assigned'' ELSE owner END) AS __group__, 
     248   id AS ticket, summary, component, version, milestone, severity, time AS created, 
     249   changetime AS _changetime, description AS _description, 
     250   reporter AS _reporter 
     251  FROM ticket t,enum p 
     252  WHERE status = ''resolved'' 
     253AND p.name=t.priority AND p.type=''priority'' 
     254  ORDER BY (IFNULL(owner, '''') = '''') DESC, owner, p.value, severity, time 
     255',' 
     256List resolved tickets, group by ticket owner, sorted by priority. 
     257'); 
     258 
     259INSERT INTO report VALUES(NULL,NULL,'Completed Tickets by Milestone (Full Description)',' 
     260SELECT p.value AS __color__, 
     261   (CASE WHEN IFNULL(milestone, '''') = '''' THEN ''Not Assigned'' ELSE milestone||'' Release'' END) AS __group__, 
     262   id AS ticket, summary, component, status, version, severity, time AS created, 
     263   description AS _description_, 
     264   changetime AS _changetime, reporter AS _reporter 
     265  FROM ticket t, enum p 
     266  WHERE status IN (''verified'', ''closed'') 
     267AND p.name = t.priority AND p.type = ''priority'' 
     268  ORDER BY (IFNULL(milestone, '''') = '''') DESC,milestone, p.value, severity, time 
     269',' 
     270Release Notes: List verified and closed tickets, group by milestone, include description. 
     271'); 
     272""" 
     273 
     274def do_upgrade(env, ver, cursor): 
     275    cursor.execute(sql) 
  • trac/upgrades/__init__.py

     
    1 __all__ = ['db2', 'db3', 'db4', 'db5', 'db6', 'db7'] 
     1__all__ = ['db2', 'db3', 'db4', 'db5', 'db6', 'db7', 'db8'] 
  • trac/Ticket.py

     
    130130 
    131131        if not self._old and not comment: return # Not modified 
    132132 
    133         # If the component is changed on a 'new' ticket then owner field 
    134         # is updated accordingly. (#623). 
    135         if self['status'] == 'new' and self._old.has_key('component') and \ 
    136                not self._old.has_key('owner'): 
    137             cursor.execute('SELECT owner FROM component ' 
    138                            'WHERE name=%s', self._old['component']) 
    139             old_owner = cursor.fetchone()[0] 
    140             if self['owner'] == old_owner: 
    141                 cursor.execute('SELECT owner FROM component ' 
    142                                'WHERE name=%s', self['component']) 
    143                 self['owner'] = cursor.fetchone()[0] 
    144             
    145  
    146133        for name in self._old.keys(): 
    147134            if name[:7] == 'custom_': 
    148135                fname = name[7:] 
     
    264251        i += 1 
    265252 
    266253 
     254def get_workflow(env, db, user): 
     255#    from trac.workflows.Simple import SimpleWorkflow 
     256#    return SimpleWorkflow(env, db, user) 
     257    modulename = env.get_config('ticket', 'workflow', \ 
     258                                'trac.workflows.SimpleWorkflow') 
     259    i = modulename.rfind('.') 
     260    if i == -1: 
     261        classname = modulename 
     262    else: 
     263        classname = modulename[i+1:] 
     264 
     265    module = __import__(modulename, globals(), locals(), [classname]) 
     266    constructor = getattr(module, classname) 
     267    workflow = constructor(env, db, user) 
     268 
     269    from workflows.Base import WorkflowBase 
     270    if not isinstance(workflow, WorkflowBase): 
     271        raise EnvironmentError, "Workflow class %s from %s must be " \ 
     272                                "descendant of class WorkflowBase from " \ 
     273                                "trac.workflows.base" \ 
     274                                % (classname, modulename) 
     275 
     276    return workflow 
     277 
     278 
    267279class NewticketModule(Module): 
    268280    template_name = 'newticket.cs' 
    269281 
    270     def create_ticket(self): 
    271         if not self.args.get('summary'): 
    272             raise util.TracError('Tickets must contain Summary.') 
    273  
    274         ticket = Ticket() 
    275         ticket.populate(self.args) 
     282    def create_ticket(self, ticket, workflow): 
    276283        ticket.setdefault('reporter',self.req.authname) 
    277284 
    278         # The owner field defaults to the component owner 
    279         cursor = self.db.cursor() 
    280         if ticket.get('component') and ticket.get('owner', '') == '': 
    281             cursor.execute('SELECT owner FROM component ' 
    282                            'WHERE name=%s', ticket['component']) 
    283             owner = cursor.fetchone()[0] 
    284             ticket['owner'] = owner 
    285  
     285        workflow.on_insert(ticket) 
    286286        tktid = ticket.insert(self.db) 
    287287 
    288288        # Notify 
     
    294294    def render (self): 
    295295        self.perm.assert_permission(perm.TICKET_CREATE) 
    296296 
    297         if self.args.has_key('create'): 
    298             self.create_ticket() 
     297        ticket = Ticket() 
    299298 
    300         ticket = Ticket() 
     299        preview = self.args.has_key('preview') 
     300        do_create = self.args.has_key('create') 
    301301        ticket.populate(self.args) 
     302 
     303        workflow = get_workflow(self.env, self.db, self.req.authname) 
     304 
     305        # Validate the ticket 
     306        err = [] 
     307        if preview or do_create: 
     308            err.extend(workflow.validate(ticket)) 
     309        if len(err) != 0: preview = 1 
     310 
     311        # Create the ticket if not in preview mode 
     312        if not preview and do_create: 
     313            workflow.do_action(ticket, 'create', self.args) 
     314            self.create_ticket(ticket, workflow) 
     315 
    302316        ticket.setdefault('component', 
    303317                          self.env.get_config('ticket', 'default_component')) 
    304318        ticket.setdefault('milestone', 
     
    320334        evals = util.mydict(zip(ticket.keys(), 
    321335                                map(lambda x: util.escape(x), ticket.values()))) 
    322336        util.add_to_hdf(evals, self.req.hdf, 'newticket') 
     337        if len(err) != 0: 
     338            self.req.hdf.setValue('newticket.workflow.error', 
     339                              wiki_to_html(' * ' + '\n * '.join(err), 
     340                                           self.req.hdf, self.env, self.db)) 
     341        tpl = workflow.get_actions_template(ticket) 
     342        if tpl: 
     343            self.req.hdf.setValue('newticket.workflow.template', tpl) 
     344            workflow.init_template(ticket, self.req.hdf) 
    323345 
    324346        util.sql_to_hdf(self.db, 'SELECT name FROM component ORDER BY name', 
    325347                        self.req.hdf, 'newticket.components') 
     
    334356class TicketModule (Module): 
    335357    template_name = 'ticket.cs' 
    336358 
    337     def save_changes (self, id): 
     359    def save_changes (self, ticket, workflow): 
    338360        self.perm.assert_permission (perm.TICKET_MODIFY) 
    339         ticket = Ticket(self.db, id) 
    340361 
    341         if not self.args.get('summary'): 
    342             raise util.TracError('Tickets must contain Summary.') 
    343  
    344362        if self.args.has_key('description'): 
    345363            self.perm.assert_permission (perm.TICKET_ADMIN) 
    346364 
    347365        if self.args.has_key('reporter'): 
    348366            self.perm.assert_permission (perm.TICKET_ADMIN) 
    349367 
    350         # TODO: this should not be hard-coded like this 
    351         action = self.args.get('action', None) 
    352         if action == 'accept': 
    353             ticket['status'] =  'assigned' 
    354             ticket['owner'] = self.req.authname 
    355         if action == 'resolve': 
    356             ticket['status'] = 'closed' 
    357             ticket['resolution'] = self.args.get('resolve_resolution') 
    358         elif action == 'reassign': 
    359             ticket['owner'] = self.args.get('reassign_owner') 
    360             ticket['status'] = 'new' 
    361         elif action == 'reopen': 
    362             ticket['status'] = 'reopened' 
    363             ticket['resolution'] = '' 
    364  
    365         ticket.populate(self.args) 
    366  
    367368        now = int(time.time()) 
    368369 
     370        workflow.on_update(ticket) 
    369371        ticket.save_changes(self.db, 
    370372                            self.args.get('author', self.req.authname), 
    371373                            self.args.get('comment'), 
     
    373375 
    374376        tn = TicketNotifyEmail(self.env) 
    375377        tn.notify(ticket, newticket=0, modtime=now) 
    376         self.req.redirect(self.env.href.ticket(id)) 
     378        self.req.redirect(self.env.href.ticket(ticket['id'])) 
    377379 
    378380    def insert_ticket_data(self, hdf, id, ticket, reporter_id): 
    379381        """Insert ticket data into the hdf""" 
     
    431433    def render (self): 
    432434        self.perm.assert_permission (perm.TICKET_VIEW) 
    433435 
    434         action = self.args.get('action', 'view') 
    435         preview = self.args.has_key('preview') 
    436  
    437436        if not self.args.has_key('id'): 
    438437            self.req.redirect(self.env.href.wiki()) 
    439438 
    440439        id = int(self.args.get('id')) 
     440        ticket = Ticket(self.db, id) 
    441441 
    442         if not preview \ 
    443                and action in ['leave', 'accept', 'reopen', 'resolve', 'reassign']: 
    444             self.save_changes (id) 
     442        action = self.args.get('action', None) 
     443        preview = self.args.has_key('preview') 
     444        if action or preview: 
     445            ticket.populate(self.args) 
    445446 
    446         ticket = Ticket(self.db, id) 
     447        workflow = get_workflow(self.env, self.db, self.req.authname) 
     448 
     449        # Validate ticket 
     450        err = [] 
     451        if action or preview: 
     452            actions = workflow.get_actions(ticket) 
     453            if action not in actions: 
     454                err.append("Invalid action '''%s''' is performed on the ticket. " \ 
     455                           "Allowed actions are <''%s''>." % \ 
     456                           (action, ', '.join(actions))) 
     457            err.extend(workflow.validate(ticket)) 
     458        if len(err) != 0: preview = 1 
     459 
     460        # Save changes if not in preview mode 
     461        if not preview and action: 
     462            workflow.do_action(ticket, action, self.args) 
     463            self.save_changes(ticket, workflow) 
     464 
    447465        reporter_id = util.get_reporter_id(self.req) 
    448466 
    449467        if preview: 
    450             # Use user supplied values 
    451             for field in Ticket.std_fields: 
    452                 if self.args.has_key(field) and field != 'reporter': 
    453                     ticket[field] = self.args.get(field) 
    454             self.req.hdf.setValue('ticket.action', action) 
     468            if action: self.req.hdf.setValue('ticket.action', action) 
    455469            reporter_id = self.args.get('author') 
    456470            comment = self.args.get('comment') 
    457471            if comment: 
     
    462476                                               self.req.hdf, self.env, self.db)) 
    463477 
    464478        self.insert_ticket_data(self.req.hdf, id, ticket, reporter_id) 
     479        if len(err) != 0: 
     480            self.req.hdf.setValue('ticket.workflow.error', 
     481                              wiki_to_html(' * ' + '\n * '.join(err), 
     482                                           self.req.hdf, self.env, self.db)) 
     483        tpl = workflow.get_actions_template(ticket) 
     484        if tpl: 
     485            self.req.hdf.setValue('ticket.workflow.template', tpl) 
     486            workflow.init_template(ticket, self.req.hdf) 
    465487 
    466488        cursor = self.db.cursor() 
    467489        cursor.execute("SELECT max(id) FROM ticket") 
  • trac/WikiFormatter.py

     
    125125            elif row[1] == 'closed': 
    126126                return '<a href="%s" title="CLOSED : %s"><del>#%d</del></a>' % (self._href.ticket(number), summary, number) 
    127127            else: 
    128                 return '<a href="%s" title="%s">#%d</a>' % (self._href.ticket(number), summary, number) 
     128                return '<a href="%s" title="%s : %s">#%d</a>' % (self._href.ticket(number), row[1].upper(), summary, number) 
    129129 
    130130    def _changesethref_formatter(self, match, fullmatch): 
    131131        number = int(match[1:-1]) 
     
    158158                elif row[1] == 'closed': 
    159159                    return self._href.ticket(args), '<del>%s:%s</del>' % (module, args), 0, 'CLOSED: ' + summary 
    160160                else: 
    161                     return self._href.ticket(args), '%s:%s' % (module, args), 0, summary 
     161                    return self._href.ticket(args), '%s:%s' % (module, args), 0, row[1].upper() + ': ' + summary 
    162162            else: 
    163163                return self._href.ticket(args), '%s:%s' % (module, args), 1, '' 
    164164        elif module == 'wiki': 
  • templates/ticket_workflow_qarmt.cs

     
     1<?cs 
     2if !ticket.action ?><?cs 
     3  set:ticket.action = 'leave' ?><?cs 
     4/if ?><?cs 
     5def action_radio(id) ?> 
     6  <input type="radio" id="<?cs var id ?>" name="action" value="<?cs var id ?>" 
     7    <?cs if $ticket.action == $id ?> checked="checked"<?cs /if ?> /><?cs 
     8/def ?> 
     9 
     10<?cs 
     11if ticket.workflow.action.leave ?><?cs 
     12  call:action_radio('leave') ?> 
     13  <label for="leave">leave as <?cs var:ticket.status ?></label><br /><?cs 
     14/if ?><?cs 
     15if ticket.workflow.action.accept ?><?cs 
     16  call action_radio('accept') ?> 
     17  <label for="accept">accept ticket</label><br /><?cs 
     18/if ?><?cs 
     19if ticket.workflow.action.resolve ?><?cs 
     20  call:action_radio('resolve') ?> 
     21  <label for="resolve">resolve</label> 
     22  <label for="resolve_resolution">as:</label><?cs 
     23  call:hdf_select(enums.resolution, "resolve_resolution", 
     24                  args.resolve_resolution) ?><br /><?cs 
     25/if ?><?cs 
     26if ticket.workflow.action.verify ?><?cs 
     27  call action_radio('verify') ?> 
     28  <label for="verify">verify ticket</label><br /><?cs 
     29/if ?><?cs 
     30if ticket.workflow.action.close ?><?cs 
     31  call action_radio('close') ?> 
     32  <label for="close">close ticket</label><br /><?cs 
     33/if ?><?cs 
     34if ticket.workflow.action.reopen ?><?cs 
     35  call:action_radio('reopen') ?> 
     36  <label for="reopen">reopen ticket</label><br /><?cs 
     37/if ?><?cs 
     38if ticket.workflow.action.retest ?><?cs 
     39  call:action_radio('retest') ?> 
     40  <label for="retest">retest ticket</label><br /><?cs 
     41/if ?><?cs 
     42if ticket.workflow.action.reassign ?><?cs 
     43  call:action_radio('reassign') ?> 
     44  <label for="reassign">reassign</label> 
     45  <label for="reassign_owner">to:</label> 
     46  <input type="text" id="reassign_owner" name="reassign_owner" size="40" 
     47    value=<?cs if args.reassign_to ?>"<?cs var:args.reassign_to ?>" 
     48          <?cs else ?>"<?cs var:trac.authname ?>" 
     49          <?cs /if ?> /><?cs 
     50/if ?> 
     51 
     52<?cs 
     53if ticket.workflow.action.resolve || ticket.workflow.action.reassign ?> 
     54  <script type="text/javascript"><?cs 
     55  if ticket.workflow.action.resolve ?> 
     56    var resolve = document.getElementById("resolve");<?cs 
     57  /if ?><?cs 
     58  if ticket.workflow.action.reassign ?> 
     59    var reassign = document.getElementById("reassign");<?cs 
     60  /if ?> 
     61    var updateActionFields = function() {<?cs 
     62  if ticket.workflow.action.resolve ?> 
     63      enableControl('resolve_resolution', resolve.checked);<?cs 
     64  /if ?><?cs 
     65  if ticket.workflow.action.reassign ?> 
     66      enableControl('reassign_owner', reassign.checked);<?cs 
     67  /if ?> 
     68    }; 
     69    addEvent(window, 'load', updateActionFields);<?cs 
     70  if ticket.workflow.action.leave ?> 
     71    addEvent(document.getElementById("leave"), 'click', updateActionFields);<?cs 
     72  /if ?><?cs 
     73  if ticket.workflow.action.accept ?> 
     74    addEvent(document.getElementById("accept"), 'click', updateActionFields);<?cs 
     75  /if ?><?cs 
     76  if ticket.workflow.action.resolve ?> 
     77    addEvent(resolve, 'click', updateActionFields);<?cs 
     78  /if ?><?cs 
     79  if ticket.workflow.action.verify ?> 
     80    addEvent(document.getElementById("verify"), 'click', updateActionFields);<?cs 
     81  /if ?><?cs 
     82  if ticket.workflow.action.close ?> 
     83    addEvent(document.getElementById("close"), 'click', updateActionFields);<?cs 
     84  /if ?><?cs 
     85  if ticket.workflow.action.reopen ?> 
     86    addEvent(document.getElementById("reopen"), 'click', updateActionFields);<?cs 
     87  /if ?><?cs 
     88  if ticket.workflow.action.retest ?> 
     89    addEvent(document.getElementById("retest"), 'click', updateActionFields);<?cs 
     90  /if ?><?cs 
     91  if ticket.workflow.action.reassign ?> 
     92    addEvent(reassign, 'click', updateActionFields);<?cs 
     93  /if ?> 
     94  </script> 
     95<?cs /if ?> 
  • templates/roadmap.cs

     
    2222      var:milestone.name ?></em></a></h2> 
    2323    <p class="date"><?cs if:milestone.date ?> 
    2424     <?cs var:milestone.date ?><?cs else ?>No date set<?cs /if ?> 
     25     <?cs if:milestone.owner ?>&nbsp;(<?cs var:milestone.owner ?>)<?cs /if ?> 
    2526    </p> 
    2627    <?cs with:stats = milestone.stats ?> 
    2728     <?cs if:#stats.total_tickets > #0 ?> 
  • templates/ticket.cs

     
    3333<div id="ticket"> 
    3434 <div class="date"><?cs var:ticket.opened ?></div> 
    3535 <h1>Ticket #<?cs var:ticket.id ?> <?cs 
    36  if:ticket.status == 'closed' ?>(Closed: <?cs var:ticket.resolution ?>)<?cs 
     36 if:ticket.resolution ?>(<?cs var:ticket.status ?>: <?cs var:ticket.resolution ?>)<?cs 
    3737 elif:ticket.status != 'new' ?>(<?cs var:ticket.status ?>)<?cs 
    3838 /if ?></h1> 
    3939 <h2><?cs var:ticket.summary ?></h2> 
     
    208208  </div><?cs /if ?> 
    209209 </fieldset> 
    210210 
    211  <fieldset id="action"> 
    212   <legend>Action</legend><?cs 
    213   if:!ticket.action ?><?cs set:ticket.action = 'leave' ?><?cs 
    214   /if ?><?cs 
    215   def:action_radio(id) ?> 
    216    <input type="radio" id="<?cs var:id ?>" name="action" value="<?cs 
    217      var:id ?>"<?cs if:$ticket.action == $id ?> checked="checked"<?cs 
    218      /if ?> /><?cs 
    219   /def ?> 
    220   <?cs call:action_radio('leave') ?> 
    221   <label for="leave">leave as <?cs var:ticket.status ?></label><br /><?cs 
    222   if $ticket.status == "new" ?> 
    223    <?cs call:action_radio('accept') ?> 
    224    <label for="accept">accept ticket</label><br /><?cs 
    225   /if ?><?cs 
    226   if $ticket.status == "closed" ?> 
    227    <?cs call:action_radio('reopen') ?> 
    228    <label for="reopen">reopen ticket</label><br /><?cs 
    229   /if ?><?cs 
    230   if $ticket.status == "new" || $ticket.status == "assigned" || $ticket.status == "reopened" ?> 
    231    <?cs call:action_radio('resolve') ?> 
    232    <label for="resolve">resolve</label> 
    233    <label for="resolve_resolution">as:</label> 
    234    <?cs call:hdf_select(enums.resolution, "resolve_resolution", args.resolve_resolution) ?><br /> 
    235    <?cs call:action_radio('reassign') ?> 
    236    <label for="reassign">reassign</label> 
    237    <label for="reassign_owner">to:</label> 
    238    <input type="text" id="reassign_owner" name="reassign_owner" size="40" value="<?cs 
    239      if:args.reassign_to ?><?cs var:args.reassign_to ?><?cs 
    240      else ?><?cs var:trac.authname ?><?cs /if ?>" /><?cs 
    241   /if ?><?cs 
    242   if $ticket.status == "new" || $ticket.status == "assigned" || $ticket.status == "reopened" ?> 
    243    <script type="text/javascript"> 
    244      var resolve = document.getElementById("resolve"); 
    245      var reassign = document.getElementById("reassign"); 
    246      var updateActionFields = function() { 
    247        enableControl('resolve_resolution', resolve.checked); 
    248        enableControl('reassign_owner', reassign.checked); 
    249      }; 
    250      addEvent(window, 'load', updateActionFields); 
    251      addEvent(document.getElementById("leave"), 'click', updateActionFields);<?cs 
    252     if $ticket.status == "new" ?> 
    253      addEvent(document.getElementById("accept"), 'click', updateActionFields);<?cs 
    254     /if ?> 
    255     addEvent(resolve, 'click', updateActionFields); 
    256     addEvent(reassign, 'click', updateActionFields); 
    257    </script><?cs 
    258   /if ?> 
    259  </fieldset> 
     211 <?cs if ticket.workflow.template ?> 
     212  <fieldset id="action"> 
     213   <legend>Action</legend> 
     214   <?cs include ticket.workflow.template ?> 
     215  </fieldset> 
     216 <?cs /if ?> 
    260217 
     218 <?cs if ticket.workflow.error ?> 
     219   <div class="system-message"> 
     220     <h2>Ticket Error</h2> 
     221     <p class="message"><?cs var ticket.workflow.error ?></p> 
     222     <strong>The ticket will not be saved.</strong> 
     223   </div> 
     224 <?cs /if ?> 
     225 
    261226 <div class="buttons"> 
    262227  <input type="reset" value="Reset" />&nbsp; 
    263228  <input type="submit" name="preview" value="Preview" />&nbsp; 
  • templates/ticket_workflow_simple.cs

     
     1<?cs 
     2if !ticket.action ?><?cs 
     3  set:ticket.action = 'leave' ?><?cs 
     4/if ?><?cs 
     5def action_radio(id) ?> 
     6  <input type="radio" id="<?cs var id ?>" name="action" value="<?cs var id ?>" 
     7    <?cs if $ticket.action == $id ?> checked="checked"<?cs /if ?> /><?cs 
     8/def ?> 
     9 
     10<?cs 
     11if ticket.workflow.action.leave ?><?cs 
     12  call:action_radio('leave') ?> 
     13  <label for="leave">leave as <?cs var:ticket.status ?></label><br /><?cs 
     14/if ?><?cs 
     15if ticket.workflow.action.accept ?><?cs 
     16  call action_radio('accept') ?> 
     17  <label for="accept">accept ticket</label><br /><?cs 
     18/if ?><?cs 
     19if ticket.workflow.action.resolve ?><?cs 
     20  call:action_radio('resolve') ?> 
     21  <label for="resolve">resolve</label> 
     22  <label for="resolve_resolution">as:</label><?cs 
     23  call:hdf_select(enums.resolution, "resolve_resolution", 
     24                  args.resolve_resolution) ?><br /><?cs 
     25/if ?><?cs 
     26if ticket.workflow.action.reopen ?><?cs 
     27  call:action_radio('reopen') ?> 
     28  <label for="reopen">reopen ticket</label><br /><?cs 
     29/if ?><?cs 
     30if ticket.workflow.action.reassign ?><?cs 
     31  call:action_radio('reassign') ?> 
     32  <label for="reassign">reassign</label> 
     33  <label for="reassign_owner">to:</label> 
     34  <input type="text" id="reassign_owner" name="reassign_owner" size="40" 
     35    value=<?cs if args.reassign_to ?>"<?cs var:args.reassign_to ?>" 
     36          <?cs else ?>"<?cs var:trac.authname ?>" 
     37          <?cs /if ?> /><?cs 
     38/if ?> 
     39 
     40<?cs 
     41if ticket.workflow.action.resolve || ticket.workflow.action.reassign ?> 
     42  <script type="text/javascript"><?cs 
     43  if ticket.workflow.action.resolve ?> 
     44    var resolve = document.getElementById("resolve");<?cs 
     45  /if ?><?cs 
     46  if ticket.workflow.action.reassign ?> 
     47    var reassign = document.getElementById("reassign");<?cs 
     48  /if ?> 
     49    var updateActionFields = function() {<?cs 
     50  if ticket.workflow.action.resolve ?> 
     51      enableControl('resolve_resolution', resolve.checked);<?cs 
     52  /if ?><?cs 
     53  if ticket.workflow.action.reassign ?> 
     54      enableControl('reassign_owner', reassign.checked);<?cs 
     55  /if ?> 
     56    }; 
     57    addEvent(window, 'load', updateActionFields);<?cs 
     58  if ticket.workflow.action.leave ?> 
     59    addEvent(document.getElementById("leave"), 'click', updateActionFields);<?cs 
     60  /if ?><?cs 
     61  if ticket.workflow.action.accept ?> 
     62    addEvent(document.getElementById("accept"), 'click', updateActionFields);<?cs 
     63  /if ?><?cs 
     64  if ticket.workflow.action.resolve ?> 
     65    addEvent(resolve, 'click', updateActionFields);<?cs 
     66  /if ?><?cs 
     67  if ticket.workflow.action.reopen ?> 
     68    addEvent(document.getElementById("reopen"), 'click', updateActionFields);<?cs 
     69  /if ?><?cs 
     70  if ticket.workflow.action.reassign ?> 
     71    addEvent(reassign, 'click', updateActionFields);<?cs 
     72  /if ?> 
     73  </script> 
     74<?cs /if ?> 
  • templates/timeline_rss.cs

     
    4343                             $item.href, $item.msg_escwiki)  
    4444        ?><?cs elif:item.type == #3 
    4545        ?><!-- Closed ticket --> <?cs call:rss_item('Ticket', 
    46                              'Ticket #'+$item.idata+' resolved: '+$item.shortmsg, 
     46                             'Ticket #'+$item.idata+' closed: '+$item.shortmsg, 
    4747                             $item.href, $item.msg_escwiki)  
    4848        ?><?cs elif:item.type == #4  
    4949        ?><!-- Reopened ticket --><?cs call:rss_item('Ticket', 
    5050                             '#'+$item.idata+' reopened: '+$item.shortmsg, 
    5151                             $item.href, $item.msg_escwiki)  
     52        ?><?cs elif:item.type == #7  
     53        ?><!-- Verified ticket --><?cs call:rss_item('Ticket', 
     54                             '#'+$item.idata+' verified: '+$item.shortmsg, 
     55                             $item.href, $item.msg_escwiki)  
     56        ?><?cs elif:item.type == #8 
     57        ?><!-- Resolved ticket --> <?cs call:rss_item('Ticket', 
     58                             'Ticket #'+$item.idata+' resolved: '+$item.shortmsg, 
     59                             $item.href, $item.msg_escwiki)  
     60        ?><?cs elif:item.type == #9  
     61        ?><!-- Retested ticket --><?cs call:rss_item('Ticket', 
     62                             '#'+$item.idata+' retested: '+$item.shortmsg, 
     63                             $item.href, $item.msg_escwiki)  
    5264        ?><?cs elif:item.type == #5  
    5365        ?><!-- Wiki change --><?cs call:rss_item('Wiki', 
    5466                             $item.tdata+" page edited.", 
     
    6274        <?cs /if ?> 
    6375      <?cs /each ?> 
    6476    </channel> 
    65 </rss> 
    66  No newline at end of file 
     77</rss> 
  • templates/newticket.cs

     
    6969  </div><?cs /if ?> 
    7070 </fieldset> 
    7171 
     72 <?cs if newticket.workflow.template ?> 
     73  <fieldset id="action"> 
     74   <legend>Action</legend> 
     75   <?cs include newticket.workflow.template ?> 
     76  </fieldset> 
     77 <?cs /if ?> 
     78 
     79 <?cs if newticket.workflow.error ?> 
     80   <div class="system-message"> 
     81     <h2>Ticket Error</h2> 
     82     <p class="message"><?cs var newticket.workflow.error ?></p> 
     83     <strong>The ticket will not be created.</strong> 
     84   </div> 
     85 <?cs /if ?> 
     86 
    7287 <div class="buttons"> 
    73   <input type="submit" value="Preview" />&nbsp; 
     88  <input type="submit" name="preview" value="Preview" />&nbsp; 
    7489  <input type="submit" name="create" value="Submit ticket" /> 
    7590 </div> 
    7691</form> 
  • templates/milestone.cs

     
    5656      var:milestone.name ?>" /> 
    5757   </div> 
    5858   <div class="field"> 
     59    <label for="owner">Owner of the milestone (Release Manager):</label><br /> 
     60    <input type="text" id="owner" name="owner" size="32" value="<?cs 
     61      var:milestone.owner ?>" /> 
     62   </div> 
     63   <div class="field"> 
    5964    <label for="datemode">Completion date:</label><br /> 
    6065    <select name="datemode" id="datemode" 
    6166        onchange="enableControl('date',this.value=='manual'); 
     
    108113 <?cs else ?> 
    109114  <em class="date"><?cs if:milestone.date ?> 
    110115   <?cs var:milestone.date ?><?cs else ?>No date set<?cs /if ?> 
     116   <?cs if:milestone.owner ?>&nbsp;(<?cs var:milestone.owner ?>)<?cs /if ?> 
    111117  </em> 
    112118  <div class="descr"><?cs var:milestone.descr ?></div> 
    113119 <?cs /if ?> 
     
    120126  <thead><tr> 
    121127   <th class="name" rowspan="2"><?cs var:milestone.stats.grouped_by ?></th> 
    122128   <th class="tickets" scope="col" colspan="2">Tickets</th> 
    123    <th class="progress" rowspan="2">Percent Resolved</th> 
     129   <th class="progress" rowspan="2">Percent Completed</th> 
    124130  </tr><tr> 
    125131   <th class="open" scope="col">Active</th> 
    126132   <th class="closed" scope="col">Closed</th> 
  • templates/timeline.cs

     
    6060 
    6161<?cs each:item = timeline.items ?> 
    6262 <?cs call:day_separator(item.date) ?> 
     63 <?cs if:item.tdata && item.message ?> 
     64  <?cs set:ticketmsg = $item.tdata + ' - ' + $item.message ?> 
     65 <?cs elif:item.tdata ?> 
     66  <?cs set:ticketmsg = $item.tdata ?> 
     67 <?cs else ?> 
     68  <?cs set:ticketmsg = $item.message ?> 
     69 <?cs /if ?> 
    6370 <?cs if:item.type == #1 ?><!-- Changeset --> 
    6471  <?cs call:tlitem(item.href, 'changeset', 
    6572    'Changeset <em>['+$item.idata+']</em> by '+$item.author,$item.node_list+item.message) ?> 
     
    6774  <?cs call:tlitem(item.href, 'newticket', 
    6875    'Ticket <em>#'+$item.idata+'</em> created by '+$item.author, item.message) ?> 
    6976 <?cs elif:item.type == #3 ?><!-- Closed ticket --> 
    70   <?cs if:item.message ?> 
    71    <?cs set:imessage = ' - ' + $item.message ?> 
    72   <?cs else ?> 
    73    <?cs set:imessage = '' ?> 
    74   <?cs /if ?> 
    7577  <?cs call:tlitem(item.href, 'closedticket', 
    76     'Ticket <em>#'+$item.idata+'</em> resolved by '+$item.author,  
    77     $item.tdata+$imessage) ?> 
     78    'Ticket <em>#'+$item.idata+'</em> closed by '+$item.author, $ticketmsg) ?> 
    7879 <?cs elif:item.type == #4 ?><!-- Reopened ticket --> 
    79   <?cs call:tlitem(item.href, 'newticket', 
    80     'Ticket <em>#'+$item.idata+'</em> reopened by '+$item.author, '') ?> 
     80  <?cs call:tlitem(item.href, 'reopenedticket', 
     81    'Ticket <em>#'+$item.idata+'</em> reopened by '+$item.author, item.message) ?> 
     82 <?cs elif:item.type == #7 ?><!-- Verified ticket --> 
     83  <?cs call:tlitem(item.href, 'closedticket', 
     84    'Ticket <em>#'+$item.idata+'</em> verified by '+$item.author, $ticketmsg) ?> 
     85 <?cs elif:item.type == #8 ?><!-- Resolved ticket --> 
     86  <?cs call:tlitem(item.href, 'resolvedticket', 
     87    'Ticket <em>#'+$item.idata+'</em> resolved by '+$item.author, $ticketmsg) ?> 
     88 <?cs elif:item.type == #9 ?><!-- Retested ticket --> 
     89  <?cs call:tlitem(item.href, 'reopenedticket', 
     90    'Ticket <em>#'+$item.idata+'</em> retested by '+$item.author, item.message) ?> 
    8191 <?cs elif:item.type == #5 ?><!-- Wiki change --> 
    8292  <?cs call:tlitem(item.href, 'wiki', 
    8393    '<em>'+$item.tdata+'</em> edited by '+$item.author, item.message) ?>