Edgewall Software

Version 29 (modified by Alec Thomas, 18 years ago) ( diff )

Minor rewording

Workflow Discussion

The original proposal is at NewWorkflow. The workflow sandbox contains a refactored ticket API which aims to allow plugins control over most aspects of the ticketing system.

Change log is here.

Tasks

Done

  • Add a disabled attribute to fields so that workflow hooks can disable fields but still leave them visible.
  • Add a hidden attribute for the same reason. I've actually already done this, simply by renaming the skip attribute to hidden. Did this to be more consistent with HTML.
  • Add a fullrow attribute which signifies that the form element will span both columns of the ticket property fieldset. eg. summary, type, description and reporter would all be fullrow=1. [2833]
  • Remove code specific to individual fields from ticket.cs/newticket.cs. The summary, type, description and reporter would be converted to use the the same generic code as the rest of the fields. [2833]
  • Remove large if/elif statement from ticket.cs/newticket.cs. Currently there is a large if/then/else style block which is used to display all fields other than the four described above. This could be removed and replaced with a call to form_control(). [2833]
  • In order to specify the order the generic field display code would use, the above changes would probably require the ticket.fields.* HDF branch to be changed to an array (currently it is a dict). This change would be in api.py (return fields in the correct display order) and the .cs files, possibly elsewhere. [2832].
  • If possible I would also like to factor out the ticket field display/edit code from both ticket.cs and newticket.cs into ticket_fields.cs, as the template code is basically functionally identical. [2833]
  • Need a clean way to differentiate between fields that should not be displayed in the summary and those that should not be displayed at all. eg. summary should be hidden in the ticket summary (as it is used for the title), and should only be editable by users with TICKET_ADMIN privileges. Perhaps the logic should be that if a field is hidden it is not displayed anywhere, unless it is also editable, in which case it is only displayed in the ticket properties. [2837]
  • Add a .plaintext option which negates the default behaviour of WikiFormatting field labels and values (see #925, #1395 and #1791 for related information). [2849]
  • Add a .title option, or maybe .tooltip, though I think for the sake of consisteny it should be .title. [2851]

TODO

  • Add an .onchange option for javascript field validation.
  • Add a checkboxes field type. This differs from the checkbox type in that it is a set of related checkboxes. It would store the data in the field value as check1|check2|check4. I'm not sure about this one, or how it would be presented in the query module, but I figured that people might find it useful. Similarly, a multi_select type could be useful.
  • Remove ticket_custom_props() macro? It does not appear to be referenced.
  • There are a number of locations in the ticket code where permissions are hard coded. As an example, the TICKET_CREATE permission is required to create a new ticket. Should this be overridable by ITicketWorkflow implementors?
  • It might be an idea to simply have a apply_ticket_changes() method instead of having apply_ticket_action() and update_ticket_fields().
  • Add a percentage field type? Represented as a bar like in the milestone:0.10 view, except if you click on a position in the bar it sets the field value. This could probably just be done by setting html_value appropriately.

Basic Configurable Workflow

Basic workflow configuration is possible in TracIni through two new sections, ticket-status and ticket-actions. This configurability is limited to defining ticket actions and status states and their transitions with basic permission enforcement.

Defining available actions for a status

Each key under the ticket-status section is a ticket status and the value associated with each key is the actions available.

[ticket-status]
assigned = leave resolve reassign
closed = leave reopen retest
new = leave resolve reassign accept
reopened = leave resolve reassign
resolved = leave reassign reopen verify
verified = leave reassign reopen retest close

Mapping actions to resulting statuses

The ticket-actions section defines what the ticket status will be for each action, in addition to the permission required for that action.

[ticket-actions]
accept = assigned
close = closed
close.permission = TRAC_ADMIN
reassign = new
reopen = reopened
reopen.permission = TICKET_ADMIN
resolve = resolved
retest = resolved
retest.permission = TICKET_ADMIN
verify = verified

Plugabble Workflow

For more complex ticket workflow requirements two extension points are available, allowing full control of the ticket workflow process.

class ITicketManipulator(Interface):
    """
    ITicketManipulator implementations are used to perform filtering of
    visible fields and validation.
    """

    def filter_fields(req, ticket, fields):
        """ Filter a list of Field objects in place. Called just prior to
            ticket display. """

    def filter_actions(req, ticket, actions):
        """ Filter a list of ticket actions and controls in place. `actions` is
            an list of tuples in the form (action, label, controls). """

    def validate_ticket(req, ticket):
        """ Validate a ticket. Called just before the ticket is updated with
            user supplied values from req.args. """

class ITicketFieldProvider(Interface):
    """ Provide custom ticket fields programmatically. """

    def get_custom_fields():
        """ Return an iterable of trac.ticket.field.Field objects. """

class ITicketWorkflow(Interface):
    """ This interface controls what actions can be performed on a ticket. """

    def get_actions(req, ticket):
        """ Return the actions that are available given the current state of
            ticket and the request object provided. """

    def get_action_controls(req, ticket, action):
        """ Return an iterable of tuples in the form (label, controls), where
            controls is a list of trac.ticket.field.Field objects. """

    def apply_action(req, ticket, action):
        """ Perform action on ticket. """

Available Field Types and Options

Common options:

  • label: Descriptive label.
  • value: Default value.
  • html_value: This option is most useful to plugins. If it exists it is the content displayed instead of the normal value. This can be useful for fields with ticket ID's, to display the correct TracLinks for the tickets.
  • order: Sort order placement. (Determines relative placement in forms.)
  • hidden: Field is not displayed. Useful for storing extra ticket attributes programmatically (false by default).
  • fullrow: Field spans a full row when displayed in the ticket properties.
  • editable: Field is editable (true by default if field is not hidden). If a field is hidden but editable, it will not display in the ticket summary but will be displayed and editable in the ticket properties.
  • plaintext: Field labels and values use WikiFormatting by default. To display plain text set this option to true.
  • title: HTML title (tooltip) for this field.

Types and type-specific options:

  • text: A simple (one line) text field.
    • size: Size of text field.
  • checkbox: A boolean value check box.
    • value: Default value (0 or 1).
  • select: Drop-down select box. Uses a list of values.
    • options: List of values, separated by | (vertical pipe).
    • value: Default value (Item #, starting at 0).
  • radio: Radio buttons. Essentially the same as select.
    • options: List of values, separated by | (vertical pipe).
    • value: Default value (Item #, starting at 0).
  • textarea: Multi-line text area.
    • cols: Width in columns.
    • rows: Height in lines.

Example Manipulator

The following manipulator adds duplicate ticket closing with a reference to the duplicate, basically implementing #1395.

class DuplicateField(Component):
    """ Allow a ticket to be closed with a reference to a ticket ID that is a
        duplicate. """

    implements(ITicketManipulator, ITicketFieldProvider)

    # ITicketFieldProvider methods
    def get_custom_fields(self):
        # 'duplicate' is where the duplicate ticket ID is stored
        yield Text('duplicate', label='Duplicate of', hidden=True)

    # ITicketManipulator methods
    def filter_fields(self, req, ticket, fields):
        # If ticket is closed as a duplicate, unhide the duplicate field
        if ticket.values.get('status') == 'closed' and ticket.values.get('resolution') == 'duplicate':
            for field in fields:
                if field['name'] == 'duplicate':
                    field['hidden'] = False
                    field['html_value'] = wiki_to_oneliner('#' + field['value'], self.env)

    def filter_actions(self, req, ticket, actions):
        # Remove 'duplicate' option from resolve, and inject "duplicate of .." control
        for idx, (action, label, controls) in enumerate(actions):
            if action == 'resolve':
                controls[0]['options'].remove('duplicate')
                value = req.args.get('duplicate_of', ticket.values.get('duplicate'))
                actions.insert(idx + 1, ('resolve_duplicate', 'duplicate',
                               (Text('duplicate_of', label='of:', hidden=False,
                                     value=value, title='Ticket ID that this is a duplicate of'),)))
                break

    def validate_ticket(self, req, ticket):
        # If resolving as duplicate, update duplicate field
        if req.args.get('action') == 'resolve_duplicate':
            dup_id = req.args.get('duplicate_of', '')
            if not dup_id:
                raise TracError('Duplicate ticket ID not provided')
            try:
                duplicate = Ticket(self.env, int(dup_id))
            except TracError:
                raise TracError('Invalid duplicate ticket ID %s' % dup_id)
            except:
                raise TracError('Invalid duplicate ticket ID %s, must be an integer' % dup_id)
            duplicate.save_changes(req.authname, '#%i has been marked as a ' \
                                   'duplicate of this ticket' % ticket.id)
            for field in ticket.fields:
                if field['name'] == 'duplicate':
                    req.args['duplicate'] = str(dup_id)
            req.args['action'] = 'resolve'
            req.args['resolve_resolution'] = 'duplicate'
        elif req.args.get('action') == 'reopen':
            ticket['duplicate'] = ''

Attachments (3)

Download all attachments as: .zip

Note: See TracWiki for help on using the wiki.