Trees Indices Help
Trac
Package trac :: Package ticket :: Module api

Source Code for Module trac.ticket.api

 1 # -*- coding: utf-8 -*- 
 2 # 
 3 # Copyright (C) 2003-2009 Edgewall Software 
 4 # Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com> 
 5 # All rights reserved. 
 6 # 
 7 # This software is licensed as described in the file COPYING, which 
 8 # you should have received as part of this distribution. The terms 
 9 # are also available at http://trac.edgewall.org/wiki/TracLicense. 
 10 # 
 11 # This software consists of voluntary contributions made by many 
 12 # individuals. For the exact contribution history, see the revision 
 13 # history and logs, available at http://trac.edgewall.org/log/. 
 14 # 
 15 # Author: Jonas Borgström <jonas@edgewall.com> 
 16 
 17 import copy 
 18 import re 
 19 
 20 from genshi.builder import tag  
 21 
 22 from trac .cache  import cached  
 23 from trac .config  import * 
 24 from trac .core  import * 
 25 from trac .perm  import IPermissionRequestor , PermissionCache, PermissionSystem  
 26 from trac .resource  import IResourceManager  
 27 from trac .util  import Ranges  
 28 from trac .util .text  import shorten_line  
 29 from trac .util .translation  import _ , N_, gettext 
 30 from trac .wiki  import IWikiSyntaxProvider , WikiParser  
31 32 33 - class ITicketActionController (Interface):
34 """Extension point interface for components willing to participate 35 in the ticket workflow. 36 37 This is mainly about controlling the changes to the ticket ''status'', 38 though not restricted to it. 39 """ 40
41 - def get_ticket_actions (req, ticket):
42 """Return an iterable of `(weight, action)` tuples corresponding to 43 the actions that are contributed by this component. 44 That list may vary given the current state of the ticket and the 45 actual request parameter. 46 47 `action` is a key used to identify that particular action. 48 (note that 'history' and 'diff' are reserved and should not be used 49 by plugins) 50 51 The actions will be presented on the page in descending order of the 52 integer weight. The first action in the list is used as the default 53 action. 54 55 When in doubt, use a weight of 0."""
56
57 - def get_all_status ():
58 """Returns an iterable of all the possible values for the ''status'' 59 field this action controller knows about. 60 61 This will be used to populate the query options and the like. 62 It is assumed that the initial status of a ticket is 'new' and 63 the terminal status of a ticket is 'closed'. 64 """
65
66 - def render_ticket_action_control (req, ticket, action):
67 """Return a tuple in the form of `(label, control, hint)` 68 69 `label` is a short text that will be used when listing the action, 70 `control` is the markup for the action control and `hint` should 71 explain what will happen if this action is taken. 72 73 This method will only be called if the controller claimed to handle 74 the given `action` in the call to `get_ticket_actions`. 75 76 Note that the radio button for the action has an `id` of 77 `"action_%s" % action`. Any `id`s used in `control` need to be made 78 unique. The method used in the default ITicketActionController is to 79 use `"action_%s_something" % action`. 80 """
81
82 - def get_ticket_changes (req, ticket, action):
83 """Return a dictionary of ticket field changes. 84 85 This method must not have any side-effects because it will also 86 be called in preview mode (`req.args['preview']` will be set, then). 87 See `apply_action_side_effects` for that. If the latter indeed triggers 88 some side-effects, it is advised to emit a warning 89 (`trac.web.chrome.add_warning(req, reason)`) when this method is called 90 in preview mode. 91 92 This method will only be called if the controller claimed to handle 93 the given `action` in the call to `get_ticket_actions`. 94 """
95
96 - def apply_action_side_effects (req, ticket, action):
97 """Perform side effects once all changes have been made to the ticket. 98 99 Multiple controllers might be involved, so the apply side-effects 100 offers a chance to trigger a side-effect based on the given `action` 101 after the new state of the ticket has been saved. 102 103 This method will only be called if the controller claimed to handle 104 the given `action` in the call to `get_ticket_actions`. 105 """
106
107 108 - class ITicketChangeListener (Interface):
109 """Extension point interface for components that require notification 110 when tickets are created, modified, or deleted.""" 111
112 - def ticket_created (ticket):
113 """Called when a ticket is created."""
114
115 - def ticket_changed (ticket, comment, author, old_values):
116 """Called when a ticket is modified. 117 118 `old_values` is a dictionary containing the previous values of the 119 fields that have changed. 120 """
121
122 - def ticket_deleted (ticket):
123 """Called when a ticket is deleted."""
124
125 126 - class ITicketManipulator (Interface):
127 """Miscellaneous manipulation of ticket workflow features.""" 128
129 - def prepare_ticket (req, ticket, fields, actions):
130 """Not currently called, but should be provided for future 131 compatibility."""
132
133 - def validate_ticket (req, ticket):
134 """Validate a ticket after it's been populated from user input. 135 136 Must return a list of `(field, message)` tuples, one for each problem 137 detected. `field` can be `None` to indicate an overall problem with the 138 ticket. Therefore, a return value of `[]` means everything is OK."""
139
140 141 - class IMilestoneChangeListener (Interface):
142 """Extension point interface for components that require notification 143 when milestones are created, modified, or deleted.""" 144
145 - def milestone_created (milestone):
146 """Called when a milestone is created."""
147
148 - def milestone_changed (milestone, old_values):
149 """Called when a milestone is modified. 150 151 `old_values` is a dictionary containing the previous values of the 152 milestone properties that changed. Currently those properties can be 153 'name', 'due', 'completed', or 'description'. 154 """
155
156 - def milestone_deleted (milestone):
157 """Called when a milestone is deleted."""
158
159 160 - class TicketSystem (Component):
161 implements (IPermissionRequestor , IWikiSyntaxProvider , IResourceManager ) 162 163 change_listeners = ExtensionPoint (ITicketChangeListener ) 164 milestone_change_listeners = ExtensionPoint (IMilestoneChangeListener ) 165 166 action_controllers = OrderedExtensionsOption ('ticket', 'workflow', 167 ITicketActionController , default ='ConfigurableTicketWorkflow', 168 include_missing=False, 169 doc="""Ordered list of workflow controllers to use for ticket actions 170 (''since 0.11'').""") 171 172 restrict_owner = BoolOption ('ticket', 'restrict_owner', 'false', 173 """Make the owner field of tickets use a drop-down menu. 174 Be sure to understand the performance implications before activating 175 this option. See 176 [TracTickets#Assign-toasDrop-DownList Assign-to as Drop-Down List]. 177 178 Please note that e-mail addresses are '''not''' obfuscated in the 179 resulting drop-down menu, so this option should not be used if 180 e-mail addresses must remain protected. 181 (''since 0.9'')""") 182 183 default_version = Option ('ticket', 'default_version', '', 184 """Default version for newly created tickets.""") 185 186 default_type = Option ('ticket', 'default_type', 'defect', 187 """Default type for newly created tickets (''since 0.9'').""") 188 189 default_priority = Option ('ticket', 'default_priority', 'major', 190 """Default priority for newly created tickets.""") 191 192 default_milestone = Option ('ticket', 'default_milestone', '', 193 """Default milestone for newly created tickets.""") 194 195 default_component = Option ('ticket', 'default_component', '', 196 """Default component for newly created tickets.""") 197 198 default_severity = Option ('ticket', 'default_severity', '', 199 """Default severity for newly created tickets.""") 200 201 default_summary = Option ('ticket', 'default_summary', '', 202 """Default summary (title) for newly created tickets.""") 203 204 default_description = Option ('ticket', 'default_description', '', 205 """Default description for newly created tickets.""") 206 207 default_keywords = Option ('ticket', 'default_keywords', '', 208 """Default keywords for newly created tickets.""") 209 210 default_owner = Option ('ticket', 'default_owner', '', 211 """Default owner for newly created tickets.""") 212 213 default_cc = Option ('ticket', 'default_cc', '', 214 """Default cc: list for newly created tickets.""") 215 216 default_resolution = Option ('ticket', 'default_resolution', 'fixed', 217 """Default resolution for resolving (closing) tickets 218 (''since 0.11'').""") 219
220 - def __init__ (self):
221 self.log .debug('action controllers for ticket workflow: %r' % 222 [c .__class__.__name__ for c in self.action_controllers ])
223 224 # Public API 225
226 - def get_available_actions (self, req, ticket):
227 """Returns a sorted list of available actions""" 228 # The list should not have duplicates. 229 actions = {} 230 for controller in self.action_controllers : 231 weighted_actions = controller.get_ticket_actions (req, ticket ) or [] 232 for weight, action in weighted_actions: 233 if action in actions: 234 actions[action] = max(actions[action], weight) 235 else: 236 actions[action] = weight 237 all_weighted_actions = [(weight, action) for action, weight in 238 actions.items()] 239 return [x [1] for x in sorted(all_weighted_actions, reverse=True)]
240
241 - def get_all_status (self):
242 """Returns a sorted list of all the states all of the action 243 controllers know about.""" 244 valid_states = set () 245 for controller in self.action_controllers : 246 valid_states.update (controller.get_all_status () or []) 247 return sorted(valid_states)
248
249 - def get_ticket_field_labels (self):
250 """Produce a (name,label) mapping from `get_ticket_fields`.""" 251 labels = dict((f['name'], f['label']) 252 for f in self.get_ticket_fields ()) 253 labels['attachment'] = _ ("Attachment") 254 return labels
255
256 - def get_ticket_fields (self):
257 """Returns list of fields available for tickets. 258 259 Each field is a dict with at least the 'name', 'label' (localized) 260 and 'type' keys. 261 It may in addition contain the 'custom' key, the 'optional' and the 262 'options' keys. When present 'custom' and 'optional' are always `True`. 263 """ 264 fields = copy.deepcopy(self.fields ) 265 label = 'label' # workaround gettext extraction bug 266 for f in fields : 267 f[label] = gettext(f[label]) 268 return fields
269
270 - def reset_ticket_fields (self):
271 """Invalidate ticket field cache.""" 272 del self.fields
273 274 @cached
275 - def fields (self, db):
276 """Return the list of fields available for tickets.""" 277 from trac .ticket import model 278 279 fields = [] 280 281 # Basic text fields 282 fields .append({'name': 'summary', 'type': 'text', 283 'label': N_('Summary')}) 284 fields .append({'name': 'reporter', 'type': 'text', 285 'label': N_('Reporter')}) 286 287 # Owner field, by default text but can be changed dynamically 288 # into a drop-down depending on configuration (restrict_owner=true) 289 field = {'name': 'owner', 'label': N_('Owner')} 290 field['type'] = 'text' 291 fields .append(field) 292 293 # Description 294 fields .append({'name': 'description', 'type': 'textarea', 295 'label': N_('Description')}) 296 297 # Default select and radio fields 298 selects = [('type', N_('Type'), model .Type ), 299 ('status', N_('Status'), model .Status ), 300 ('priority', N_('Priority'), model .Priority ), 301 ('milestone', N_('Milestone'), model .Milestone ), 302 ('component', N_('Component'), model .Component ), 303 ('version', N_('Version'), model .Version ), 304 ('severity', N_('Severity'), model .Severity ), 305 ('resolution', N_('Resolution'), model .Resolution )] 306 for name , label, cls in selects: 307 options = [val.name for val in cls.select (self.env , db =db )] 308 if not options : 309 # Fields without possible values are treated as if they didn't 310 # exist 311 continue 312 field = {'name': name , 'type': 'select', 'label': label, 313 'value': getattr(self, 'default_' + name , ''), 314 'options': options } 315 if name in ('status', 'resolution'): 316 field['type'] = 'radio' 317 field['optional'] = True 318 elif name in ('milestone', 'version'): 319 field['optional'] = True 320 fields .append(field) 321 322 # Advanced text fields 323 fields .append({'name': 'keywords', 'type': 'text', 324 'label': N_('Keywords')}) 325 fields .append({'name': 'cc', 'type': 'text', 'label': N_('Cc')}) 326 327 # Date/time fields 328 fields .append({'name': 'time', 'type': 'time', 329 'label': N_('Created')}) 330 fields .append({'name': 'changetime', 'type': 'time', 331 'label': N_('Modified')}) 332 333 for field in self.get_custom_fields (): 334 if field['name'] in [f['name'] for f in fields ]: 335 self.log .warning('Duplicate field name "%s" (ignoring)', 336 field['name']) 337 continue 338 if field['name'] in self.reserved_field_names : 339 self.log .warning('Field name "%s" is a reserved name ' 340 '(ignoring)', field['name']) 341 continue 342 if not re.match('^[a-zA-Z][a-zA-Z0-9_]+$', field['name']): 343 self.log .warning('Invalid name for custom field: "%s" ' 344 '(ignoring)', field['name']) 345 continue 346 field['custom'] = True 347 fields .append(field) 348 349 return fields
350 351 reserved_field_names = ['report', 'order', 'desc', 'group', 'groupdesc', 352 'col', 'row', 'format', 'max', 'page', 'verbose', 353 'comment', 'or'] 354
355 - def get_custom_fields (self):
356 return copy.deepcopy(self.custom_fields )
357 358 @cached
359 - def custom_fields (self, db):
360 """Return the list of custom ticket fields available for tickets.""" 361 fields = [] 362 config = self.config ['ticket-custom'] 363 for name in [option for option, value in config .options () 364 if '.' not in option]: 365 field = { 366 'name': name , 367 'type': config .get (name ), 368 'order': config .getint (name + '.order', 0), 369 'label': config .get (name + '.label') or name .capitalize(), 370 'value': config .get (name + '.value', '') 371 } 372 if field['type'] == 'select' or field['type'] == 'radio': 373 field['options'] = config .getlist (name + '.options', sep='|') 374 if '' in field['options']: 375 field['optional'] = True 376 field['options'].remove ('') 377 elif field['type'] == 'text': 378 field['format'] = config .get (name + '.format', 'plain') 379 elif field['type'] == 'textarea': 380 field['format'] = config .get (name + '.format', 'plain') 381 field['width'] = config .getint (name + '.cols') 382 field['height'] = config .getint (name + '.rows') 383 fields .append(field) 384 385 fields .sort(lambda x , y: cmp((x ['order'], x ['name']), 386 (y['order'], y['name']))) 387 return fields
388
389 - def get_field_synonyms (self):
390 """Return a mapping from field name synonyms to field names. 391 The synonyms are supposed to be more intuitive for custom queries.""" 392 # i18n TODO - translated keys 393 return {'created': 'time', 'modified': 'changetime'}
394
395 - def eventually_restrict_owner (self, field, ticket=None):
396 """Restrict given owner field to be a list of users having 397 the TICKET_MODIFY permission (for the given ticket) 398 """ 399 if self.restrict_owner : 400 field['type'] = 'select' 401 possible_owners = [] 402 for user in PermissionSystem (self.env ) \ 403 .get_users_with_permission ('TICKET_MODIFY'): 404 if not ticket or \ 405 'TICKET_MODIFY' in PermissionCache(self.env , user, 406 ticket .resource ): 407 possible_owners.append(user) 408 possible_owners.sort() 409 field['options'] = possible_owners 410 field['optional'] = True
411 412 # IPermissionRequestor methods 413
414 - def get_permission_actions (self):
415 return ['TICKET_APPEND', 'TICKET_CREATE', 'TICKET_CHGPROP', 416 'TICKET_VIEW', 'TICKET_EDIT_CC', 'TICKET_EDIT_DESCRIPTION', 417 'TICKET_EDIT_COMMENT', 418 ('TICKET_MODIFY', ['TICKET_APPEND', 'TICKET_CHGPROP']), 419 ('TICKET_ADMIN', ['TICKET_CREATE', 'TICKET_MODIFY', 420 'TICKET_VIEW', 'TICKET_EDIT_CC', 421 'TICKET_EDIT_DESCRIPTION', 422 'TICKET_EDIT_COMMENT'])]
423 424 # IWikiSyntaxProvider methods 425 430
431 - def get_wiki_syntax (self):
432 yield ( 433 # matches #... but not &#... (HTML entity) 434 r"!?(?<!&)#" 435 # optional intertrac shorthand #T... + digits 436 r"(?P<it_ticket>%s)%s" % (WikiParser .INTERTRAC_SCHEME , 437 Ranges .RE_STR ), 438 lambda x , y, z : self._format_link(x , 'ticket', y[1:], y, z ))
439 474 495 496 # IResourceManager methods 497
498 - def get_resource_realms (self):
499 yield 'ticket'
500
501 - def get_resource_description (self, resource, format=None, context=None, 502 **kwargs):
503 if format == 'compact': 504 return '#%s' % resource .id 505 elif format == 'summary': 506 from trac .ticket .model import Ticket 507 ticket = Ticket (self.env , resource .id ) 508 args = [ticket [f] for f in ('summary', 'status', 'resolution', 509 'type')] 510 return self.format_summary (*args ) 511 return _ ("Ticket #%(shortname)s", shortname=resource .id )
512
513 - def format_summary (self, summary, status=None, resolution=None, type=None):
514 summary = shorten_line (summary) 515 if type : 516 summary = type + ': ' + summary 517 if status: 518 if status == 'closed' and resolution: 519 status += ': ' + resolution 520 return "%s (%s)" % (summary, status) 521 else: 522 return summary
523
524 - def resource_exists (self, resource):
525 """ 526 >>> from trac.test import EnvironmentStub 527 >>> from trac.resource import Resource, resource_exists 528 >>> env = EnvironmentStub() 529 530 >>> resource_exists(env, Resource('ticket', 123456)) 531 False 532 533 >>> from trac.ticket.model import Ticket 534 >>> t = Ticket(env) 535 >>> int(t.insert()) 536 1 537 >>> resource_exists(env, t.resource) 538 True 539 """ 540 db = self.env .get_read_db () 541 cursor = db .cursor () 542 cursor .execute ("SELECT id FROM ticket WHERE id=%s", (resource .id ,)) 543 latest_exists = bool(cursor .fetchall ()) 544 if latest_exists: 545 if resource .version is None: 546 return True 547 cursor .execute (""" 548 SELECT count(distinct time) FROM ticket_change WHERE ticket=%s 549 """, (resource .id ,)) 550 return cursor .fetchone ()[0] >= resource .version 551 else: 552 return False
553

Trees Indices Help
Trac
Generated by Epydoc 3.0.1 on Mon Feb 13 23:37:32 2023 http://epydoc.sourceforge.net

AltStyle によって変換されたページ (->オリジナル) /