My company is in the process of selecting a set of tools for their
next CM system and as part of the Open Source solution I needed to
Demo Trac with supporting multiple workflows, custom permissions for
transitions, and custom ticket properties for each work flow.  There
was also a requirement for some of these properties to be writable or
required to be filled in in order to allow the transition to occur.

Well in order to have Trac support multiple work flows I just decided
to make one workflow that consisted of all the workflows.  So if you
had for example the following three workflows

workflow A => new -> A1 -> A2 -> closed
workflow B => new -> B1 -> B2 -> closed
workflow C => new -> C1 -> C2 -> closed

you could create a workflow like the following (will look better with
fix font)

   /->newA -> A1 -> A2 \
new ->newB -> B1 -> B2 -> closed
   \->newC -> C1 -> C2 /

So the idea is that once the ticket is created the next available
transition would be to select a ticket type and once the ticket type
was selected the desired workflow for that ticket type would be in
effect.  Now it's not really the ideal solution but it does work today
without changing any of Trac's code.  With some changes to the UI it
would be possible to just have a user create a ticket of the right
type and automatically advance from the new state to the newstate of
the required workflow.  If this is in place the user doesn't even have
to be aware that only one big workflow exists.  Although the best long
term solution for Trac would be to just support multiple workflows.

To add custom permissions I just created a plugin with code similar to

from trac.core import *
from trac.perm import IPermissionRequestor

class CMTrac(Component):
    implements(IPermissionRequestor)

    # IPermissionRequestor method
    def get_permission_actions(self):
        return ['ORIGINATOR',
                'DESIGNER',
                'REQUIREMENTS_TEAM',
                'SOFTWARE_DEVELOPER',
                'REQUIREMENTS_LEAD',
                'LOGIC_DESIGNER',
                'SW_BUILD_MANAGER',
                'KEY_REVIEWER',
                'SW_TECH_LEAD',
                'DEVELOPER']

With this code you also need the rest of the boiler plate code that is
required for plugins.  If needed just see Trac's Wiki for more
information on making Trac plugins.

To handle the requirements of having different ticket properties
having required and writable states for different transitions I just
created a spreadsheet whose first row was headers simailar to

from, to, property1, property2, property3...

After the header row was a row for each valid transition with the from
column having the name of the from state and the to column for you
guessed it the to state.  Under the property columns it would either
be left blank or set to either required or writable.  Writable would
indicated that the user would be able to change the property and
required would mean that the property could not be left blank to
perform the transition.  Once the spreadsheet was filled out it was
exported to a csv file.

The following code was used to parse the csv files and to create 3
dictionaries called formFields, enabledWhen, and requiredWhen.  The
key of formFields would be the name of a state and the values would be
a set that contains the name of properties that are required to be
displayed in the ticket form for the given state.  The key of
enabledWhen would be the name of a transition in the form of from_to
and the values would be a set that contains the name of the properties
that are either writable or required for a given transition.  The
requiredWhen dict would be similar to enabledWhen but the sets would
only contain the required properties.

import csv
import os.path

attributesFilename = os.path.join(os.path.dirname(__file__),
'poperties.csv')
reader = csv.reader(file(attributesFilename, "rb"))

rows = [row for row in reader]

headings = rows[0]

formFields = {}
enabledWhen = {}
requiredWhen = {}


for row in rows[1:]:
    for i, value in enumerate(row):
        if 'Required' in value or 'Writeable' in value:
            state = row[0]
            s = formFields.get(state, set())
            s.add(headings[i])
            formFields[state] = s

for row in rows[1:]:
    for i, value in enumerate(row):
        if 'Required' in value:
            transition = '%s_%s' % (row[0], row[1])
            s = requiredWhen.get(transition, set())
            s.add(headings[i])
            requiredWhen[transition] = s

for row in rows[1:]:
    for i, value in enumerate(row):
        if 'Required' in value or 'Writeable' in value:
            transition = '%s_%s' % (row[0], row[1])
            s = enabledWhen.get(transition, set())
            s.add(headings[i])
            enabledWhen[transition] = s

The ticket template needed to be modified in order for this new
functionality to be available.  Now some of the changes were just
hacked in just to get the demo to work like <?python?> at the top of
the template that imports three above mentioned dicts as they should
be passed into the template instead.  The other hack is the fact that
no server side validation is occurring.  These are issues that can
easily be fixed with a little work but didn't get to as there was only
2 weeks to put the demo together and there were many other
requirements that needed to be meet besides these workflow issues.
Any way I hope this helps and sorry for the long email.

John

modified ticket template follows...

<!DOCTYPE html
    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd";>
<html xmlns="http://www.w3.org/1999/xhtml";
      xmlns:py="http://genshi.edgewall.org/";
      xmlns:xi="http://www.w3.org/2001/XInclude";>
  <xi:include href="layout.html" />
  <xi:include href="macros.html" />

<?python
import custom.workflow.ticket_fields as tf

requiredWhen = tf.requiredWhen
formFields  = tf.formFields
enabledWhen = tf.enabledWhen
variablesThatShouldBeJsonStreams = [('requiredWhen', requiredWhen),
('formFields', formFields), ('enabledWhen', enabledWhen)]

?>

  <head >
    <title>
      <py:choose>
        <py:when test="ticket.exists">
          #${ticket.id} (${ticket.summary})
        </py:when>
        <py:otherwise>
          New Ticket
        </py:otherwise>
      </py:choose>
    </title>
    <script type="text/javascript" src="${chrome.htdocs_location}js/
wikitoolbar.js"></script>
    <script type="text/javascript" py:choose="">
      $(document).ready(function() {
        $("div.description").find("h1,h2,h3,h4,h5,h6").addAnchor("Link
to this section");
      <py:when test="ticket.exists">
        $("#changelog h3.change").addAnchor("Link to this change");

        /* only enable control elements for the currenly selected
action */
        var actions = $("[EMAIL PROTECTED]'action']//
[EMAIL PROTECTED]'action']");
        /* ... as Opera doesn't like $("#action [EMAIL PROTECTED]")
*/

        function updateActionFields() {
          actions.each(function () {
            $(this).siblings().find("[EMAIL PROTECTED]").enable($
(this).checked());
            $(this).siblings().filter("[EMAIL PROTECTED]").enable($
(this).checked());
          });
        }
        actions.click(updateActionFields);
        updateActionFields();
      </py:when>
      <py:otherwise>
        $(document).ready(function() {$("#field-
summary").get(0).focus()});
      </py:otherwise>
      });
    </script>
<script type="text/javascript">
status = '${ticket.status}'
type = '${ticket.type}'
resolution = '${ticket.resolution}'

<py:for each="name, variable in variablesThatShouldBeJsonStreams">
    $name = {}
    <py:for each="key, item in variable.iteritems()">
        ${name}['$key'] = []
        <py:for each="value in item">
            ${name}['$key'] = ${name}['$key'].concat(['$value'])
        </py:for>
    </py:for>
</py:for>

function showFields(key, nextState){
    console.log(nextState)
    key = nextState //.toUpperCase()
    s = status //.toUpperCase()
    console.log(status)

    console.log(key)
    //console.log(requiredWhen[status+'_'+key)
    //console.log(enabledWhen[status+'_'+key])
    //console.log(formFields[status])
    requiredFields = requiredWhen[s+'_'+key]
    enabledFields  = enabledWhen[s+'_'+key]
    allFields      = formFields[s]

    console.log(requiredFields)
    console.log(enabledFields)
    console.log(allFields)

        makeReadOnly(formFields)
        highlightRequired(requiredFields)
        enableWritableFields(enabledFields)
}

//resets all the fields once a transition action is selected
function makeReadOnly(formFields)
{
    for (key in formFields)
    {
                for (item in formFields[key])
                {
                        //console.log("DISABLING field-"+allFields[key][item])
                        document.getElementById("field-"+formFields[key]
[item]).disabled=true

                        //remove highlights from old required fields
                        document.getElementById("label-"+formFields[key]
[item]).style.color="black"
                }
    }
}

function highlightRequired(requiredFields)
{
    for (key in requiredFields)
    {
        
document.getElementById("label-"+requiredFields[key]).style.color="red"
    }
}

function enableWritableFields(enabledFields)
{
    for (key in enabledFields)
    {
                
document.getElementById("field-"+enabledFields[key]).disabled=false
    }
}

function enableTracDefaults()
{
        document.getElementById("field-component").disabled=false
        document.getElementById("field-milestone").disabled=false
        document.getElementById("field-priority").disabled=false
        document.getElementById("field-type").disabled=false
        document.getElementById("field-version").disabled=false
        document.getElementById("field-keywords").disabled=false
        document.getElementById("field-cc").disabled=false
}
</script>
  </head>

  <body>
    <div id="ctxtnav" class="nav" py:with="links = chrome.links">
      <h2>Ticket Navigation</h2>
      ${prevnext_nav('Ticket', 'Back to Query')}
    </div>

    <py:def function="commentref(prefix, cnum)">
      <a href="#comment:$cnum"><small>$prefix$cnum</small></a>
    </py:def>

    <py:def function="display_change(change)">
      <ul py:if="change.fields" class="changes">
        <li py:for="field_name, field in change.fields.items()"
            py:with="field_type = field_types.get(field_name)">
          <strong>${field_name}</strong>
          <py:choose>
            <py:when test="field_name == 'attachment'">
              <a href="${href.attachment('ticket', ticket.id,
field.new)}"><em>${field.new}</em></a> added
            </py:when>
            <py:when test="field.old and field.new">
              changed
              <py:choose>
                <py:when test="field_type == 'textarea'">
                  (<a py:strip="'cnum' not in change" href="$
{href.ticket(ticket.id, action='diff', version=change.cnum)}">diff</
a>)
                </py:when>
                <py:otherwise>
                  from <em>${field.old}</em> to <em>${field.new}</em>
                </py:otherwise>
              </py:choose>
            </py:when>
            <py:when test="not field.old and field.new">
              set
              <py:if test="field_type != 'textarea'">
                to <em>${field.new}</em>
              </py:if>
            </py:when>
            <py:otherwise>deleted</py:otherwise>
          </py:choose>
        </li>
      </ul>
      <div py:if="'comment' in change" class="comment searchable"
xml:space="preserve">
        ${wiki_to_html(context, change.comment)}
      </div>
    </py:def>

    <div id="content" class="ticket" py:with="preview_mode = 'preview'
in req.args">
      <h1>
        <py:choose>
          <py:when test="ticket.exists">
            <a py:strip="not version and version != 0" href="$
{href.ticket(ticket.id)}">
              Ticket #${ticket.id}
            </a>
          </py:when>
          <py:otherwise>
            Create New Ticket
          </py:otherwise>
        </py:choose>

        <py:if test="ticket.exists">
          <span class="status">(${ticket.status} <py:if
              test="ticket.type">${ticket.type}</py:if><py:if
              test="ticket.resolution">: ${ticket.resolution}</
py:if>)</span>
          <py:choose test="">
            <py:when test="version is None" />
            <py:when test="version == 0">
              &mdash; at <a href="#comment:description">Initial
Version</a>
            </py:when>
            <py:otherwise>
              &mdash; at <a href="#comment:$version">Version $version</
a>
            </py:otherwise>
          </py:choose>
        </py:if>
      </h1>

      <!-- Do not show the ticket (pre)view when the user first comes
to the "New Ticket" page.
           Wait until they hit preview. -->
      <fieldset id="preview" py:strip="not preview_mode">
        <py:if test="preview_mode">
          <legend>Preview (<a href="#${ticket.exists and 'edit' or
'properties'}">skip</a>)</legend>

          <!-- Preview of ticket changes -->
          <div py:if="change_preview" id="ticketchange"
class="ticketdraft">
            <h3 class="change" id="${'cnum' in change_preview and
'comment:%d' % change_preview.cnum or None}">
              <span class="threading" py:if="'replyto' in
change_preview">
                in reply to: ${commentref('&darr;&nbsp;',
change_preview.replyto)}
              </span>
              Changed by ${authorinfo(change_preview.author)}
            </h3>
            ${display_change(change_preview)}
          </div>

          <span py:if="not valid" class="preview-notice">Note: this
ticket can't be saved, see the <a href="#warning"><em>warnings</em></
a> above</span>
        </py:if>

        <!-- Ticket Box (ticket fields along with description) -->
        <div id="ticket" py:if="ticket.exists or preview_mode"
          class="${preview_mode and 'ticketdraft' or None}">
          <div class="date">
            <p py:if="ticket.exists">Opened $
{dateinfo(ticket.time_created)} ago</p>
            <p py:if="ticket.time_changed != ticket.time_created">Last
modified ${dateinfo(ticket.time_changed)} ago</p>
            <p py:if="not ticket.exists"><i>(future ticket)</i></p>
          </div>
          <!-- use a placeholder if it's a new ticket -->
          <h2 class="summary searchable">${ticket.summary}</h2>

          <table class="properties" id="table_description_old"
                 py:with="fields = [f for f in fields if not f.skip]">
            <tr id="row_description_old">
              <th id="h_reporter">Reported by:</th>
              <td headers="h_reporter" class="searchable">$
{authorinfo(ticket.reporter)}</td>
              <th id="h_owner">Owned by:</th>
              <td headers="h_owner">${ticket.owner and
authorinfo(ticket.owner) or ''}
              </td>
            </tr>
          </table>
            <div class="description">
              <h3 id="comment:description">
                Description
                <span py:if="description_change" class="lastmod"
                      title="$description_change.date">
                  (last modified by $
{authorinfo(description_change.author)})
                  (<a href="${href.ticket(ticket.id, action='diff',
version=description_change.cnum)}">diff</a>)
                </span>
              </h3>

              <!--! Clone the ticket (only for existing tickets) -->
              <form py:if="ticket.exists and 'TICKET_ADMIN' in
req.perm" method="post" action="${href.newticket()}">
                <div class="inlinebuttons">
                  <input type="submit" name="clone" value="Clone"
title="Create a copy of this ticket"  />
                  <py:for each="f in fields">
                    <py:choose test="f.name">
                      <input py:when="'summary'" type="hidden"
name="field_summary" value="$ticket.summary (cloned)" />
                      <input py:when="'description'" type="hidden"
name="field_description" value="Cloned from #$ticket.id: ${'\n----\n'
+ ticket.description}" />
                      <input py:otherwise="" type="hidden" name="field_
${f.name}" value="${ticket[f.name]}" />
                    </py:choose>
                  </py:for>
                </div>
              </form>

              <!--! Quote the description (only for existing tickets)
-->
              <form py:if="ticket.exists and ticket.description and
'TICKET_APPEND' in perm"
                method="get" action="#comment">
                <div class="inlinebuttons">
                  <input type="hidden" name="replyto"
value="description" />
                  <input type="submit" name="reply" value="Reply"
title="Reply, quoting this description" />
                </div>
              </form>
              <div py:if="ticket.description" class="searchable"
xml:space="preserve">
                ${wiki_to_html(context, ticket.description)}
              </div>
            </div>
        </div>
      </fieldset>
      <!--! End of ticket box -->

      <py:if test="ticket.exists">
        <!--! do not show attachments for old versions of this ticket
or for new tickets -->
        <py:if test="not version and version != 0 and ticket.exists">
          ${list_of_attachments(attachments)}
        </py:if>

        <py:if test="ticket.exists and changes">
          <h2>Change History</h2>
          <div id="changelog">
            <form py:for="change in changes" method="get"
action="#comment" class="printableform">
              <div class="change">
                <h3 class="change" id="${'cnum' in change and 'comment:
%d' % change.cnum or None}">
                  <span class="threading" py:if="replies and 'cnum' in
change"
                        py:with="change_replies =
replies.get(str(change.cnum), [])">
                    <py:if test="change_replies or 'replyto' in
change">
                      <py:if test="'replyto' in change">
                        in reply to: ${commentref('&uarr;&nbsp;',
change.replyto)}
                        <py:if test="change_replies">; </py:if>
                      </py:if>
                      <py:if test="change_replies">follow-up$
{len(change_replies) > 1 and 's' or ''}:
                        <py:for each="reply in change_replies">
                          ${commentref('&darr;&nbsp;', reply)}
                        </py:for></py:if>
                    </py:if>
                    &nbsp;
                  </span>
                  Changed ${dateinfo(change.date)} ago by $
{authorinfo(change.author)}
                </h3>
                <div py:if="'cnum' in change and 'TICKET_APPEND' in
perm" class="inlinebuttons">
                  <input type="hidden" name="replyto" value="$
{change.cnum}" />
                  <input type="submit" value="Reply" title="Reply to
comment ${change.cnum}" />
                </div>
                ${display_change(change)}
              </div>
            </form>
          </div>
        </py:if>
      </py:if>
      <!--! End of the section we don't show on initial new tickets --
>

      <form py:if="not version and version != 0 and ('TICKET_APPEND'
in perm or 'TICKET_CHGPROP' in perm or ('TICKET_CREATE' in perm and
not ticket.id))"
            action="#preview" method="post">
        <!--! Workflow support -->
        <py:if test="ticket.id"> <!--! do not display the actions for
New tickets -->
          <fieldset id="action">
            <legend>Action</legend>
            <div py:for="key, label, controls, hints in
action_controls">
              <input type="radio" id="$key" name="action" value="$key"
checked="${action == key or None}" onclick="showFields('$key', '$
{hints[0][21:-1]}');"/>
                <label for="$key">$label</label>
                $controls
                <span class="hint" py:for="hint in hints">$hint</span>
            </div>
          </fieldset>
        </py:if>

        <h3 py:if="ticket.exists"><a id="edit" onfocus="$
('#comment').get(0).focus()">
            Add/Change #${ticket.id} (${ticket.summary})</a></h3>
        <div py:if="authname == 'anonymous'" class="field">
          <fieldset>
            <legend>${ticket.exists and 'Author' or 'Reporter'}</
legend>
            <table>
              <tr>
                <th>
                  <label for="author">Your email or username:</
label><br />
                </th>
                <td>
                  <input type="text" id="author" name="author"
size="40" value="$author_id" />
                  <br />
                </td>
              </tr>
            </table>
          </fieldset>
        </div>
        <div py:if="ticket.exists" class="field">
          <fieldset class="iefix">
            <label for="comment">Comment (you may use
              <a tabindex="42" href="$
{href.wiki('WikiFormatting')}">WikiFormatting</a>
              here):
            </label><br />
            <p><textarea id="comment" name="comment" class="wikitext"
rows="10" cols="78">
${comment}</textarea></p>
          </fieldset>
        </div>

        <fieldset id="properties"
                  py:if="'TICKET_CHGPROP' in req.perm or (not
ticket.exists and 'TICKET_CREATE' in req.perm)"
                  py:with="fields = [f for f in fields if not
f.skip]">
          <legend>${ticket.exists and 'Change ' or ''}Properties</
legend>
          <table id="table_description_new">
            <tr>
              <th><label for="field-summary">Summary:</label></th>
              <td class="fullrow" colspan="3">
                <input type="text" id="field-summary"
name="field_summary"
                       value="${ticket.summary}" size="70" />
              </td>
            </tr>
            <py:if test="'TICKET_ADMIN' in perm">
              <tr>
                <th><label for="field-reporter">Reporter:</label></th>
                <td class="fullrow" colspan="3">
                  <input type="text" id="field-reporter"
name="field_reporter"
                         value="${ticket.reporter}" size="70" />
                </td>
              </tr>
            </py:if>
            <py:if test="'TICKET_ADMIN' in perm or not ticket.exists">
              <tr id="row_description_new">
                <th><label for="field-description">Description:</
label></th>
                <td class="fullrow" colspan="3">
                  <textarea id="field-description"
name="field_description"
                            class="wikitext" rows="10" cols="68"
                            py:content="ticket.description"></
textarea>
                </td>
              </tr>
            </py:if>
            <tr py:for="row in group(fields, 2, lambda f: f.type !=
'textarea')"
                py:with="fullrow = len(row) == 1">
              <py:for each="idx, field in enumerate(row)">
                                  <th class="col${idx + 1}" py:if="idx == 0 or 
not fullrow">
                  <label for="field-${field.name}" id="label-$
{field.name}" py:if="field"
                         py:strip="field.type == 'radio'">$
{field.label or field.name}:</label>
                                  </th>
                <td class="col${idx + 1}" py:if="idx == 0 or not
fullrow"
                    py:attrs="{'colspan': fullrow and 3 or None}">
                  <py:choose test="field.type" py:if="field">
                    <select py:when="'select'" id="field-$
{field.name}" name="field_${field.name}" disabled='true'>
                      <option py:if="field.optional"></option>
                      <option py:for="option in field.options"
                              selected="${ticket[field.name] == option
or None}"
                              py:content="option"></option>
                    </select>
                    <textarea py:when="'textarea'" id="field-$
{field.name}" name="field_${field.name}"
                              cols="${field.width}" rows="$
{field.height}" disabled='true'
                              py:content="ticket[field.name]"></
textarea>
                    <span py:when="'checkbox'">
                      <input type="checkbox" id="field-${field.name}"
name="field_${field.name}"
                             checked="${ticket[field.name] == '1' and
'checked' or None}" value="1" disabled='true'/>
                      <input type="hidden" name="field_checkbox_$
{field.name}" value="1" disabled='true'/>
                    </span>
                    <label py:when="'radio'"
                           py:for="idx, option in
enumerate(field.options)">
                      <input type="radio" name="field_${field.name}"
value="${option}"
                             checked="${ticket[field.name] == option
or None}" disabled='true'/>
                      ${option}
                    </label>
                    <input py:otherwise="" type="text" id="field-$
{field.name}"
                           name="field_${field.name}" disabled='true'
value="${ticket[field.name]} " />
                  </py:choose>
                </td>
              </py:for>
            </tr>
          </table>

        </fieldset>

        <p py:if="not ticket.exists and 'ATTACHMENT_CREATE' in
req.perm(context('attachment'))">
          <label>
            <input type="checkbox" name="attachment" checked="$
{'attachment' in req.args or None}" />
            I have files to attach to this ticket
          </label>
        </p>
        <div class="buttons">
          <input py:if="not ticket.exists" type="hidden"
name="field_status" value="new" />
          <py:if test="ticket.exists">
            <input type="hidden" name="ts" value="${timestamp}" />
            <input type="hidden" name="replyto" value="${replyto}" />
            <input type="hidden" name="cnum" value="${cnum}" />
          </py:if>
          <input type="submit" name="preview" value="Preview"
accesskey="r" />&nbsp;
          <input type="submit" py:attrs="ticket.exists and
{'value':'Submit changes'} or {'value':'Create ticket'}" />
        </div>

      </form>
          <script type="text/javascript">
                enableTracDefaults()
          </script>

      <div  onload="enableTracDefaults()" id="help">
        <strong>Note:</strong> See
        <a href="${href.wiki('TracTickets')}">TracTickets</a> for help
on using
        tickets.
      </div>
    </div>
  </body>
</html>


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups "Trac 
Development" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/trac-dev?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to