keithsalisbury-subtrac 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (139) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +7 -0
  3. data/Rakefile +56 -0
  4. data/VERSION.yml +4 -0
  5. data/bin/subtrac +40 -0
  6. data/lib/subtrac.rb +245 -0
  7. data/lib/subtrac/common/clients/index.wsgi +89 -0
  8. data/lib/subtrac/common/favicon.ico +0 -0
  9. data/lib/subtrac/common/images/trac/banner_bg.jpg +0 -0
  10. data/lib/subtrac/common/images/trac/bar_bg.gif +0 -0
  11. data/lib/subtrac/common/images/trac/footer_back.png +0 -0
  12. data/lib/subtrac/common/images/trac/main_bg.gif +0 -0
  13. data/lib/subtrac/common/images/trac/saint_logo_small.png +0 -0
  14. data/lib/subtrac/common/static/404.html +14 -0
  15. data/lib/subtrac/common/styles/trac.css +222 -0
  16. data/lib/subtrac/common/trac.ini +178 -0
  17. data/lib/subtrac/config/config.yml +54 -0
  18. data/lib/subtrac/passwords +1 -0
  19. data/lib/subtrac/shared/trac.ini +178 -0
  20. data/lib/subtrac/templates/location.erb +16 -0
  21. data/lib/subtrac/templates/projects/blank/svn/branches/README +0 -0
  22. data/lib/subtrac/templates/projects/blank/svn/tags/README +0 -0
  23. data/lib/subtrac/templates/projects/blank/svn/trunk/README +0 -0
  24. data/lib/subtrac/templates/projects/blank/trac/wiki/WikiStart +57 -0
  25. data/lib/subtrac/templates/projects/new/svn/trunk/trac/wiki/WikiStart +46 -0
  26. data/lib/subtrac/templates/projects/new/trac/wiki/WikiStart +23 -0
  27. data/lib/subtrac/templates/projects/trac_theme/svn/trunk/index/index.html +22 -0
  28. data/lib/subtrac/templates/projects/trac_theme/svn/trunk/templates/layout.html +56 -0
  29. data/lib/subtrac/templates/projects/trac_theme/svn/trunk/templates/site.html +27 -0
  30. data/lib/subtrac/templates/projects/trac_theme/svn/trunk/templates/theme.html +86 -0
  31. data/lib/subtrac/templates/projects/trac_theme/trac/wiki/WikiStart +4 -0
  32. data/lib/subtrac/templates/trac.erb +25 -0
  33. data/lib/subtrac/templates/vhost.erb +35 -0
  34. data/lib/subtrac/trac-plugins/advancedticketworkflowplugin/advancedworkflow/__init__.py +0 -0
  35. data/lib/subtrac/trac-plugins/advancedticketworkflowplugin/advancedworkflow/controller.py +419 -0
  36. data/lib/subtrac/trac-plugins/advancedticketworkflowplugin/setup.cfg +3 -0
  37. data/lib/subtrac/trac-plugins/advancedticketworkflowplugin/setup.py +20 -0
  38. data/lib/subtrac/trac-plugins/clientsplugin/clients/__init__.py +0 -0
  39. data/lib/subtrac/trac-plugins/clientsplugin/clients/action.py +28 -0
  40. data/lib/subtrac/trac-plugins/clientsplugin/clients/action_email.py +168 -0
  41. data/lib/subtrac/trac-plugins/clientsplugin/clients/action_zendesk_forum.py +137 -0
  42. data/lib/subtrac/trac-plugins/clientsplugin/clients/admin.py +91 -0
  43. data/lib/subtrac/trac-plugins/clientsplugin/clients/api.py +199 -0
  44. data/lib/subtrac/trac-plugins/clientsplugin/clients/client.py +105 -0
  45. data/lib/subtrac/trac-plugins/clientsplugin/clients/events.py +287 -0
  46. data/lib/subtrac/trac-plugins/clientsplugin/clients/eventsadmin.py +71 -0
  47. data/lib/subtrac/trac-plugins/clientsplugin/clients/htdocs/clients.css +4 -0
  48. data/lib/subtrac/trac-plugins/clientsplugin/clients/model.py +135 -0
  49. data/lib/subtrac/trac-plugins/clientsplugin/clients/processor.py +70 -0
  50. data/lib/subtrac/trac-plugins/clientsplugin/clients/reportmanager.py +142 -0
  51. data/lib/subtrac/trac-plugins/clientsplugin/clients/reports.py +231 -0
  52. data/lib/subtrac/trac-plugins/clientsplugin/clients/summary.py +27 -0
  53. data/lib/subtrac/trac-plugins/clientsplugin/clients/summary_milestone.py +152 -0
  54. data/lib/subtrac/trac-plugins/clientsplugin/clients/summary_ticketchanges.py +160 -0
  55. data/lib/subtrac/trac-plugins/clientsplugin/clients/templates/admin_client_events.html +124 -0
  56. data/lib/subtrac/trac-plugins/clientsplugin/clients/templates/admin_clients.html +134 -0
  57. data/lib/subtrac/trac-plugins/clientsplugin/cron/changes.xslt +132 -0
  58. data/lib/subtrac/trac-plugins/clientsplugin/cron/run-client-event +97 -0
  59. data/lib/subtrac/trac-plugins/clientsplugin/cron/summary.xslt +161 -0
  60. data/lib/subtrac/trac-plugins/clientsplugin/setup.py +43 -0
  61. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/__init__.py +4 -0
  62. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/burndownchart.py +273 -0
  63. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/hoursinplaceeditor.py +44 -0
  64. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/hoursremaining.py +36 -0
  65. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/htdocs/jquery-1.2.3.min.js +32 -0
  66. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/htdocs/jquery.jeditable.js +409 -0
  67. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/htdocs/jquery.jeditable.mini.js +30 -0
  68. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/templates/edithours.html +53 -0
  69. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/tests/burndownchart.py +181 -0
  70. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/tests/hoursremaining.py +66 -0
  71. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/tests/workloadchart.py +47 -0
  72. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/utils.py +93 -0
  73. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/workloadchart.py +86 -0
  74. data/lib/subtrac/trac-plugins/estimationtoolsplugin/setup.py +20 -0
  75. data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/SumRollups.js +23 -0
  76. data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/adw_tracdb.py +128 -0
  77. data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/git-post-receive +40 -0
  78. data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/trac-post-commit.py +285 -0
  79. data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/trac_billing.py +173 -0
  80. data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/utils/__init__.py +0 -0
  81. data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/utils/mail.py +164 -0
  82. data/lib/subtrac/trac-plugins/timingandestimationplugin/setup.py +69 -0
  83. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/__init__.py +1 -0
  84. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/api.py +292 -0
  85. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/blackmagic.py +172 -0
  86. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/dbhelper.py +178 -0
  87. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/billingplugin.css +25 -0
  88. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/field_disabler.js +6 -0
  89. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/formatDate.js +356 -0
  90. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/js/tip_centerwindow.js +100 -0
  91. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/js/tip_followscroll.js +84 -0
  92. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/js/wz_tooltip.js +1149 -0
  93. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/linkifyer.js +119 -0
  94. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/query.js +73 -0
  95. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/ticket.js +165 -0
  96. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/query_webui.py +28 -0
  97. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/reportmanager.py +221 -0
  98. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/reports.py +675 -0
  99. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/reports_filter.py +150 -0
  100. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/statuses.py +25 -0
  101. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/tande_filters.py +131 -0
  102. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/templates/billing.cs +84 -0
  103. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/templates/billing.html +104 -0
  104. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/ticket_daemon.py +194 -0
  105. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/ticket_policy.py +62 -0
  106. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/ticket_webui.py +28 -0
  107. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/usermanual.py +127 -0
  108. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/webui.py +129 -0
  109. data/lib/subtrac/trac-plugins/worklogplugin/setup.py +29 -0
  110. data/lib/subtrac/trac-plugins/worklogplugin/worklog/__init__.py +1 -0
  111. data/lib/subtrac/trac-plugins/worklogplugin/worklog/api.py +187 -0
  112. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/jqModal.css +40 -0
  113. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/jqModal.js +67 -0
  114. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/jquery.mousewheel.pack.js +12 -0
  115. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/jquery.timeentry.pack.js +7 -0
  116. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/tracWorklog.js +40 -0
  117. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/ui.datepicker.css +208 -0
  118. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/ui.datepicker.js +1439 -0
  119. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/work.png +0 -0
  120. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/work.xcf +0 -0
  121. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/worklogplugin.css +80 -0
  122. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/workstart.png +0 -0
  123. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/workstop.png +0 -0
  124. data/lib/subtrac/trac-plugins/worklogplugin/worklog/manager.py +336 -0
  125. data/lib/subtrac/trac-plugins/worklogplugin/worklog/reports.py +598 -0
  126. data/lib/subtrac/trac-plugins/worklogplugin/worklog/templates/worklog.html +45 -0
  127. data/lib/subtrac/trac-plugins/worklogplugin/worklog/templates/worklog_stop.html +70 -0
  128. data/lib/subtrac/trac-plugins/worklogplugin/worklog/templates/worklog_user.html +40 -0
  129. data/lib/subtrac/trac-plugins/worklogplugin/worklog/templates/worklog_webadminui.html +59 -0
  130. data/lib/subtrac/trac-plugins/worklogplugin/worklog/ticket_daemon.py +33 -0
  131. data/lib/subtrac/trac-plugins/worklogplugin/worklog/ticket_filter.py +153 -0
  132. data/lib/subtrac/trac-plugins/worklogplugin/worklog/timeline_hook.py +96 -0
  133. data/lib/subtrac/trac-plugins/worklogplugin/worklog/usermanual.py +29 -0
  134. data/lib/subtrac/trac-plugins/worklogplugin/worklog/util.py +31 -0
  135. data/lib/subtrac/trac-plugins/worklogplugin/worklog/webadminui.py +47 -0
  136. data/lib/subtrac/trac-plugins/worklogplugin/worklog/webui.py +174 -0
  137. data/lib/subtrac/trac-plugins/worklogplugin/worklog/xmlrpc.py +73 -0
  138. data/lib/subtrac/version.rb +4 -0
  139. metadata +191 -0
@@ -0,0 +1,86 @@
1
+ <!DOCTYPE html
2
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
3
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
4
+ <html xmlns="http://www.w3.org/1999/xhtml"
5
+ xmlns:xi="http://www.w3.org/2001/XInclude"
6
+ xmlns:py="http://genshi.edgewall.org/"
7
+ py:strip="">
8
+
9
+ <div py:def="navigation(category)" id="${category}" class="nav">
10
+ <ul py:if="chrome.nav[category]">
11
+ <li py:for="idx, item in enumerate(chrome.nav[category])"
12
+ class="${classes(first_last(idx, chrome.nav[category]), active=item.active)
13
+ }">${item.label}</li>
14
+ </ul>
15
+ </div>
16
+
17
+ <py:match path="body" once="true" buffer="false"><body>
18
+ <div id="banner">
19
+ <div id="header" py:choose="">
20
+ <a py:when="chrome.logo.src" id="logo" href="${chrome.logo.link or href.wiki(
21
+ 'TracIni')+'#header_logo-section'}"><img
22
+ src="${chrome.logo.src}" alt="${chrome.logo.alt}"
23
+ height="${chrome.logo.height or None}" width="${chrome.logo.width or None}"
24
+ /></a>
25
+ <h1 py:otherwise=""><a href="${chrome.logo.link}">${project.name}</a></h1>
26
+ </div>
27
+ <form id="search" action="${href.search()}" method="get">
28
+ <div py:if="'SEARCH_VIEW' in perm">
29
+ <label for="proj-search">Search:</label>
30
+ <input type="text" id="proj-search" name="q" size="18" accesskey="f" value=
31
+ "" />
32
+ <input type="submit" value="Search" />
33
+ </div>
34
+ </form>
35
+ ${navigation('metanav')}
36
+ </div>
37
+ ${navigation('mainnav')}
38
+
39
+ <div id="main">
40
+ <div id="ctxtnav" class="nav">
41
+ <h2>Context Navigation</h2>
42
+ <ul py:if="chrome.ctxtnav">
43
+ <li py:for="i, elm in enumerate(chrome.ctxtnav)"
44
+ class="${classes(first_last(i, chrome.ctxtnav))}">$elm</li>
45
+ </ul>
46
+ <hr />
47
+ </div>
48
+ <div id="warning" py:if="chrome.warnings" class="system-message">
49
+ <py:choose test="len(chrome.warnings)">
50
+ <py:when test="1">
51
+ <strong>Warning:</strong> ${chrome.warnings[0]}
52
+ </py:when>
53
+ <py:otherwise>
54
+ <strong>Warnings:</strong>
55
+ <ul><li py:for="w in chrome.warnings">$w</li></ul>
56
+ </py:otherwise>
57
+ </py:choose>
58
+ </div>
59
+ <div id="notice" py:if="chrome.notices" class="system-message">
60
+ <py:choose test="len(chrome.notices)">
61
+ <py:when test="1">
62
+ <strong>Notice:</strong> ${chrome.notices[0]}
63
+ </py:when>
64
+ <py:otherwise>
65
+ <strong>Notices:</strong>
66
+ <ul><li py:for="w in chrome.notices">$w</li></ul>
67
+ </py:otherwise>
68
+ </py:choose>
69
+ </div>
70
+
71
+ ${select('*|text()')}
72
+ </div>
73
+
74
+ <div id="footer" xml:lang="en"><hr/>
75
+ <a id="tracpowered" href="http://trac.edgewall.org/"><img
76
+ src="${chrome.htdocs_location}trac_logo_mini.png" height="30"
77
+ width="107" alt="Trac Powered"/></a>
78
+ <p class="left">
79
+ Powered by <a href="${href.about()}"><strong>Trac ${trac.version}</strong></a
80
+ ><br />
81
+ By <a href="http://www.edgewall.org/">Edgewall Software</a>.
82
+ </p>
83
+ <p class="right">${chrome.footer}</p>
84
+ </div>
85
+ </body></py:match>
86
+ </html>
@@ -0,0 +1,4 @@
1
+ = This is the Trac Templates project =
2
+
3
+ This project contains the set of files that are used for creating new Trac projects for {CLIENT_NAME}.
4
+
@@ -0,0 +1,25 @@
1
+ [inherit]
2
+ file = <%= @APP_CONFIG[:dirs][:document] %>/common/trac.ini
3
+ templates_dir = ../../trac_theme/shared/theme/templates
4
+
5
+ [project]
6
+ name = <%= project_name %>
7
+
8
+ [trac]
9
+ repository_dir = ../../../svn/<%= client_name %>/<%= project_name %>
10
+
11
+ [components]
12
+ batchmod.web_ui.batchmodifymodule = enabled
13
+ ldapplugin.* = enabled
14
+
15
+ [ldap]
16
+ enable = <%= @APP_CONFIG[:ldap][:enable] %>
17
+ host = <%= @APP_CONFIG[:ldap][:host] %>
18
+ port = <%= @APP_CONFIG[:ldap][:port] %>
19
+ basedn = <%= @APP_CONFIG[:ldap][:basedn] %>
20
+ user_rdn = <%= @APP_CONFIG[:ldap][:user_rdn] %>
21
+ group_rdn = <%= @APP_CONFIG[:ldap][:group_rdn] %>
22
+ store_bind = <%= @APP_CONFIG[:ldap][:store_bind] %>
23
+ group_bind = <%= @APP_CONFIG[:ldap][:group_bind] %>
24
+ bind_user = <%= @APP_CONFIG[:ldap][:bind_user] %>
25
+ bind_passwd = <%= @APP_CONFIG[:ldap][:bind_user] %>
@@ -0,0 +1,35 @@
1
+ <virtualhost *>
2
+ ServerAdmin webmaster@localhost
3
+ ServerName <%= @APP_CONFIG[:server][:hostname] %>
4
+ DocumentRoot <%= @APP_CONFIG[:dirs][:document] %>
5
+ ErrorLog <%= @APP_CONFIG[:dirs][:log] %>/error.<%= @APP_CONFIG[:server][:hostname] %>.log
6
+ CustomLog <%= @APP_CONFIG[:dirs][:log] %>/access.<%= @APP_CONFIG[:server][:hostname] %>.log combined
7
+ LogLevel error
8
+
9
+ RewriteEngine On
10
+ RewriteRule ^/$ http://<%= @APP_CONFIG[:server][:hostname] %>/saint/public
11
+ RewriteRule ^/index*$ http://<%= @APP_CONFIG[:server][:hostname] %>/saint/public
12
+
13
+ <location />
14
+ AuthType Basic
15
+ AuthName 'SaintDigitalSourceRepository'
16
+ AuthUserFile <%= @APP_CONFIG[:dirs][:install] %>/passwords
17
+ #Require valid-user
18
+ AuthBasicProvider file ldap
19
+ AuthLDAPBindDN 'CN=WTMS LDAP Auth,OU=WTMS,OU=STS,OU=Europe,OU=EMEA-YR-LON-GLH,OU=EMEA-YR-LON,OU=EMEA-YR,DC=emea,DC=corp,DC=yr,DC=com'
20
+ AuthLDAPBindPassword 'l00kmeup'
21
+ AuthLDAPURL 'ldap://emea-root03.emea.corp.yr.com:389/dc=emea,dc=corp,dc=yr,dc=com?sAMAccountName?sub?(&(objectClass=user)(memberOf=cn=EMEA-YR-LON-GLH-SAINT-DEVELOPERS,ou=Groups,ou=Saint,ou=RKCRYR,ou=EMEA-YR-LON-GLH,ou=EMEA-YR-LON,ou=EMEA-YR,dc=emea,dc=corp,dc=yr,dc=com))'
22
+ AuthzLDAPAuthoritative off
23
+ </location>
24
+
25
+ <location <%= @APP_CONFIG[:urls][:svn] %>>
26
+ DAV svn
27
+ SVNParentPath <%= @APP_CONFIG[:dirs][:svn] %>/
28
+ SVNListParentPath On
29
+ SVNAutoversioning On
30
+ SVNReposName 'SaintDigitalSourceRepository'
31
+ </location>
32
+
33
+ Include <%= @APP_CONFIG[:dirs][:conf_locations] %>/*
34
+
35
+ </virtualhost>
@@ -0,0 +1,419 @@
1
+ """Trac plugin that provides a number of advanced operations for customizable
2
+ workflows.
3
+ """
4
+
5
+ import os
6
+ import time
7
+ from subprocess import call
8
+ from genshi.builder import tag
9
+
10
+ from trac.core import implements, Component
11
+ from trac.ticket import model
12
+ from trac.ticket.api import ITicketActionController
13
+ from trac.ticket.default_workflow import ConfigurableTicketWorkflow
14
+
15
+
16
+ class TicketWorkflowOpBase(Component):
17
+ """Abstract base class for 'simple' ticket workflow operations."""
18
+
19
+ implements(ITicketActionController)
20
+ abstract = True
21
+
22
+ _op_name = None # Must be specified.
23
+
24
+ # ITicketActionController methods
25
+
26
+ def get_ticket_actions(self, req, ticket):
27
+ """Finds the actions that use this operation"""
28
+ controller = ConfigurableTicketWorkflow(self.env)
29
+ return controller.get_actions_by_operation_for_req(req, ticket,
30
+ self._op_name)
31
+
32
+ def get_all_status(self):
33
+ """Provide any additional status values"""
34
+ # We don't have anything special here; the statuses will be recognized
35
+ # by the default controller.
36
+ return []
37
+
38
+ # This should most likely be overridden to be more functional
39
+ def render_ticket_action_control(self, req, ticket, action):
40
+ """Returns the action control"""
41
+ actions = ConfigurableTicketWorkflow(self.env).actions
42
+ label = actions[action]['name']
43
+ return (label, tag(''), '')
44
+
45
+ def get_ticket_changes(self, req, ticket, action):
46
+ """Must be implemented in subclasses"""
47
+ raise NotImplementedError
48
+
49
+ def apply_action_side_effects(self, req, ticket, action):
50
+ """No side effects"""
51
+ pass
52
+
53
+
54
+ class TicketWorkflowOpOwnerReporter(TicketWorkflowOpBase):
55
+ """Sets the owner to the reporter of the ticket.
56
+
57
+ needinfo = * -> needinfo
58
+ needinfo.name = Need info
59
+ needinfo.operations = set_owner_to_reporter
60
+
61
+
62
+ Don't forget to add the `TicketWorkflowOpOwnerReporter` to the workflow
63
+ option in [ticket].
64
+ If there is no workflow option, the line will look like this:
65
+
66
+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerReporter
67
+ """
68
+
69
+ _op_name = 'set_owner_to_reporter'
70
+
71
+ # ITicketActionController methods
72
+
73
+ def render_ticket_action_control(self, req, ticket, action):
74
+ """Returns the action control"""
75
+ actions = ConfigurableTicketWorkflow(self.env).actions
76
+ label = actions[action]['name']
77
+ hint = 'The owner will change to %s' % ticket['reporter']
78
+ control = tag('')
79
+ return (label, control, hint)
80
+
81
+ def get_ticket_changes(self, req, ticket, action):
82
+ """Returns the change of owner."""
83
+ return {'owner': ticket['reporter']}
84
+
85
+
86
+ class TicketWorkflowOpOwnerComponent(TicketWorkflowOpBase):
87
+ """Sets the owner to the default owner for the component.
88
+
89
+ <someaction>.operations = set_owner_to_component_owner
90
+
91
+ Don't forget to add the `TicketWorkflowOpOwnerComponent` to the workflow
92
+ option in [ticket].
93
+ If there is no workflow option, the line will look like this:
94
+
95
+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerComponent
96
+ """
97
+
98
+ _op_name = 'set_owner_to_component_owner'
99
+
100
+ # ITicketActionController methods
101
+
102
+ def render_ticket_action_control(self, req, ticket, action):
103
+ """Returns the action control"""
104
+ actions = ConfigurableTicketWorkflow(self.env).actions
105
+ label = actions[action]['name']
106
+ hint = 'The owner will change to %s' % self._new_owner(ticket)
107
+ control = tag('')
108
+ return (label, control, hint)
109
+
110
+ def get_ticket_changes(self, req, ticket, action):
111
+ """Returns the change of owner."""
112
+ return {'owner': self._new_owner(ticket)}
113
+
114
+ def _new_owner(self, ticket):
115
+ """Determines the new owner"""
116
+ component = model.Component(self.env, name=ticket['component'])
117
+ self.env.log.debug("component %s, owner %s" % (component, component.owner))
118
+ return component.owner
119
+
120
+
121
+ class TicketWorkflowOpOwnerField(TicketWorkflowOpBase):
122
+ """Sets the owner to the value of a ticket field
123
+
124
+ <someaction>.operations = set_owner_to_field
125
+ <someaction>.set_owner_to_field = myfield
126
+
127
+ Don't forget to add the `TicketWorkflowOpOwnerField` to the workflow
128
+ option in [ticket].
129
+ If there is no workflow option, the line will look like this:
130
+
131
+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerField
132
+ """
133
+
134
+ _op_name = 'set_owner_to_field'
135
+
136
+ # ITicketActionController methods
137
+
138
+ def render_ticket_action_control(self, req, ticket, action):
139
+ """Returns the action control"""
140
+ actions = ConfigurableTicketWorkflow(self.env).actions
141
+ label = actions[action]['name']
142
+ hint = 'The owner will change to %s' % self._new_owner(action, ticket)
143
+ control = tag('')
144
+ return (label, control, hint)
145
+
146
+ def get_ticket_changes(self, req, ticket, action):
147
+ """Returns the change of owner."""
148
+ return {'owner': self._new_owner(action, ticket)}
149
+
150
+ def _new_owner(self, action, ticket):
151
+ """Determines the new owner"""
152
+ # Should probably do some sanity checking...
153
+ field = self.config.get('ticket-workflow',
154
+ action + '.' + self._op_name).strip()
155
+ return ticket[field]
156
+
157
+
158
+ class TicketWorkflowOpOwnerPrevious(TicketWorkflowOpBase):
159
+ """Sets the owner to the previous owner
160
+
161
+ Don't forget to add the `TicketWorkflowOpOwnerPrevious` to the workflow
162
+ option in [ticket].
163
+ If there is no workflow option, the line will look like this:
164
+
165
+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpOwnerPrevious
166
+ """
167
+
168
+ _op_name = 'set_owner_to_previous'
169
+
170
+ # ITicketActionController methods
171
+
172
+ def render_ticket_action_control(self, req, ticket, action):
173
+ """Returns the action control"""
174
+ actions = ConfigurableTicketWorkflow(self.env).actions
175
+ label = actions[action]['name']
176
+ new_owner = self._new_owner(ticket)
177
+ if new_owner:
178
+ hint = 'The owner will change to %s' % new_owner
179
+ else:
180
+ hint = 'The owner will be deleted.'
181
+ control = tag('')
182
+ return (label, control, hint)
183
+
184
+ def get_ticket_changes(self, req, ticket, action):
185
+ """Returns the change of owner."""
186
+ return {'owner': self._new_owner(ticket)}
187
+
188
+ def _new_owner(self, ticket):
189
+ """Determines the new owner"""
190
+ db = self.env.get_db_cnx()
191
+ cursor = db.cursor()
192
+ cursor.execute("SELECT oldvalue FROM ticket_change WHERE ticket=%s " \
193
+ "AND field='owner' ORDER BY -time", (ticket.id, ))
194
+ row = cursor.fetchone()
195
+ if row:
196
+ owner = row[0]
197
+ else: # The owner has never changed.
198
+ owner = ''
199
+ return owner
200
+
201
+
202
+ class TicketWorkflowOpStatusPrevious(TicketWorkflowOpBase):
203
+ """Sets the status to the previous status
204
+
205
+ Don't forget to add the `TicketWorkflowOpStatusPrevious` to the workflow
206
+ option in [ticket].
207
+ If there is no workflow option, the line will look like this:
208
+
209
+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpStatusPrevious
210
+ """
211
+
212
+ _op_name = 'set_status_to_previous'
213
+
214
+ # ITicketActionController methods
215
+
216
+ def render_ticket_action_control(self, req, ticket, action):
217
+ """Returns the action control"""
218
+ actions = ConfigurableTicketWorkflow(self.env).actions
219
+ label = actions[action]['name']
220
+ new_status = self._new_status(ticket)
221
+ if new_status != ticket['status']:
222
+ hint = 'The status will change to %s' % new_status
223
+ else:
224
+ hint = ''
225
+ control = tag('')
226
+ return (label, control, hint)
227
+
228
+ def get_ticket_changes(self, req, ticket, action):
229
+ """Returns the change of status."""
230
+ return {'status': self._new_status(ticket)}
231
+
232
+ def _new_status(self, ticket):
233
+ """Determines the new status"""
234
+ db = self.env.get_db_cnx()
235
+ cursor = db.cursor()
236
+ cursor.execute("SELECT oldvalue FROM ticket_change WHERE ticket=%s " \
237
+ "AND field='status' ORDER BY -time", (ticket.id, ))
238
+ row = cursor.fetchone()
239
+ if row:
240
+ status = row[0]
241
+ else: # The status has never changed.
242
+ status = 'new'
243
+ return status
244
+
245
+
246
+ class TicketWorkflowOpRunExternal(Component):
247
+ """Action to allow running an external command as a side-effect.
248
+
249
+ If it is a lengthy task, it should daemonize so the webserver can get back
250
+ to doing its thing. If the script exits with a non-zero return code, an
251
+ error will be logged to the Trac log.
252
+ The plugin will look for a script named <tracenv>/hooks/<someaction>, and
253
+ will pass it 2 parameters: the ticket number, and the user.
254
+
255
+ <someaction>.operations = run_external
256
+ <someaction>.run_external = Hint for the user
257
+
258
+ Don't forget to add the `TicketWorkflowOpRunExternal` to the workflow
259
+ option in [ticket].
260
+ If there is no workflow option, the line will look like this:
261
+
262
+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpRunExternal
263
+ """
264
+
265
+ implements(ITicketActionController)
266
+
267
+ # ITicketActionController methods
268
+
269
+ def get_ticket_actions(self, req, ticket):
270
+ """Finds the actions that use this operation"""
271
+ controller = ConfigurableTicketWorkflow(self.env)
272
+ return controller.get_actions_by_operation_for_req(req, ticket,
273
+ 'run_external')
274
+
275
+ def get_all_status(self):
276
+ """Provide any additional status values"""
277
+ # We don't have anything special here; the statuses will be recognized
278
+ # by the default controller.
279
+ return []
280
+
281
+ def render_ticket_action_control(self, req, ticket, action):
282
+ """Returns the action control"""
283
+ actions = ConfigurableTicketWorkflow(self.env).actions
284
+ label = actions[action]['name']
285
+ hint = self.config.get('ticket-workflow',
286
+ action + '.run_external').strip()
287
+ if hint is None:
288
+ hint = "Will run external script."
289
+ return (label, tag(''), hint)
290
+
291
+ def get_ticket_changes(self, req, ticket, action):
292
+ """No changes to the ticket"""
293
+ return {}
294
+
295
+ def apply_action_side_effects(self, req, ticket, action):
296
+ """Run the external script"""
297
+ script = os.path.join(self.env.path, 'hooks', action)
298
+ retval = call([script, str(ticket.id), req.authname])
299
+ if retval:
300
+ self.env.log.error("External script %r exited with return code %s." % (script, retval))
301
+
302
+
303
+ class TicketWorkflowOpTriage(TicketWorkflowOpBase):
304
+ """Action to split a workflow based on a field
305
+
306
+ <someaction> = somestatus -> *
307
+ <someaction>.operations = triage
308
+ <someaction>.triage_field = type
309
+ <someaction>.traige_split = defect -> new_defect, task -> new_task, enhancement -> new_enhancement
310
+
311
+ Don't forget to add the `TicketWorkflowOpTriage` to the workflow option in
312
+ [ticket].
313
+ If there is no workflow option, the line will look like this:
314
+
315
+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpTriage
316
+ """
317
+
318
+ _op_name = 'triage'
319
+
320
+ # ITicketActionController methods
321
+
322
+ def render_ticket_action_control(self, req, ticket, action):
323
+ """Returns the action control"""
324
+ actions = ConfigurableTicketWorkflow(self.env).actions
325
+ label = actions[action]['name']
326
+ new_status = self._new_status(ticket, action)
327
+ if new_status != ticket['status']:
328
+ hint = 'The status will change.'
329
+ else:
330
+ hint = ''
331
+ control = tag('')
332
+ return (label, control, hint)
333
+
334
+ def get_ticket_changes(self, req, ticket, action):
335
+ """Returns the change of status."""
336
+ return {'status': self._new_status(ticket, action)}
337
+
338
+ def _new_status(self, ticket, action):
339
+ """Determines the new status"""
340
+ field = self.config.get('ticket-workflow',
341
+ action + '.triage_field').strip()
342
+ transitions = self.config.get('ticket-workflow',
343
+ action + '.triage_split').strip()
344
+ for transition in [x.strip() for x in transitions.split(',')]:
345
+ value, status = [y.strip() for y in transition.split('->')]
346
+ if value == ticket[field].strip():
347
+ break
348
+ else:
349
+ self.env.log.error("Bad configuration for 'triage' operation in action '%s'" % action)
350
+ status = 'new'
351
+ return status
352
+
353
+
354
+ class TicketWorkflowOpXRef(TicketWorkflowOpBase):
355
+ """Adds a cross reference to another ticket
356
+
357
+ <someaction>.operations = xref
358
+ <someaction>.xref = "Ticket %s is related to this ticket"
359
+ <someaction>.xref_local = "Ticket %s was marked as related to this ticket"
360
+ <someaction>.xref_hint = "The specified ticket will be cross-referenced with this ticket"
361
+
362
+ The example values shown are the default values.
363
+ Don't forget to add the `TicketWorkflowOpXRef` to the workflow
364
+ option in [ticket].
365
+ If there is no workflow option, the line will look like this:
366
+
367
+ workflow = ConfigurableTicketWorkflow,TicketWorkflowOpXRef
368
+ """
369
+
370
+ _op_name = 'xref'
371
+
372
+ # ITicketActionController methods
373
+
374
+ def render_ticket_action_control(self, req, ticket, action):
375
+ """Returns the action control"""
376
+ id = 'action_%s_xref' % action
377
+ ticketnum = req.args.get(id, '')
378
+ actions = ConfigurableTicketWorkflow(self.env).actions
379
+ label = actions[action]['name']
380
+ hint = actions[action].get('xref_hint',
381
+ 'The specified ticket will be cross-referenced with this ticket')
382
+ control = tag.input(type='text', id=id, name=id, value=ticketnum)
383
+ return (label, control, hint)
384
+
385
+ def get_ticket_changes(self, req, ticket, action):
386
+ """Returns no changes."""
387
+ return {}
388
+
389
+ def apply_action_side_effects(self, req, ticket, action):
390
+ """Add a cross-reference comment to the other ticket"""
391
+ # TODO: This needs a lot more error checking.
392
+ id = 'action_%s_xref' % action
393
+ ticketnum = req.args.get(id).strip('#')
394
+ actions = ConfigurableTicketWorkflow(self.env).actions
395
+ author = req.authname
396
+
397
+ # Add a comment to the "remote" ticket to indicate this ticket is
398
+ # related to it.
399
+ format_string = actions[action].get('xref',
400
+ 'Ticket %s is related to this ticket')
401
+ comment = format_string % ('#%s' % ticket.id)
402
+ # FIXME: This assumes the referenced ticket exists.
403
+ xticket = model.Ticket(self.env, ticketnum)
404
+ # FIXME: We _assume_ we have sufficient permissions to comment on the
405
+ # other ticket.
406
+ xticket.save_changes(author, comment)
407
+
408
+ # Add a comment to this ticket to indicate that the "remote" ticket is
409
+ # related to it. (But only if <action>.xref_local was set in the
410
+ # config.)
411
+ format_string = actions[action].get('xref_local',
412
+ 'Ticket %s was marked as related to this ticket')
413
+ if format_string:
414
+ comment = format_string % ('#%s' % ticketnum)
415
+ time.sleep(1) # FIXME: Hack around IntegrityError
416
+ # HACK: Grab a new ticket object to avoid getting
417
+ # "OperationalError: no such column: new"
418
+ xticket = model.Ticket(self.env, ticket.id)
419
+ xticket.save_changes(author, comment)