keithsalisbury-subtrac 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +7 -0
- data/Rakefile +56 -0
- data/VERSION.yml +4 -0
- data/bin/subtrac +40 -0
- data/lib/subtrac.rb +245 -0
- data/lib/subtrac/common/clients/index.wsgi +89 -0
- data/lib/subtrac/common/favicon.ico +0 -0
- data/lib/subtrac/common/images/trac/banner_bg.jpg +0 -0
- data/lib/subtrac/common/images/trac/bar_bg.gif +0 -0
- data/lib/subtrac/common/images/trac/footer_back.png +0 -0
- data/lib/subtrac/common/images/trac/main_bg.gif +0 -0
- data/lib/subtrac/common/images/trac/saint_logo_small.png +0 -0
- data/lib/subtrac/common/static/404.html +14 -0
- data/lib/subtrac/common/styles/trac.css +222 -0
- data/lib/subtrac/common/trac.ini +178 -0
- data/lib/subtrac/config/config.yml +54 -0
- data/lib/subtrac/passwords +1 -0
- data/lib/subtrac/shared/trac.ini +178 -0
- data/lib/subtrac/templates/location.erb +16 -0
- data/lib/subtrac/templates/projects/blank/svn/branches/README +0 -0
- data/lib/subtrac/templates/projects/blank/svn/tags/README +0 -0
- data/lib/subtrac/templates/projects/blank/svn/trunk/README +0 -0
- data/lib/subtrac/templates/projects/blank/trac/wiki/WikiStart +57 -0
- data/lib/subtrac/templates/projects/new/svn/trunk/trac/wiki/WikiStart +46 -0
- data/lib/subtrac/templates/projects/new/trac/wiki/WikiStart +23 -0
- data/lib/subtrac/templates/projects/trac_theme/svn/trunk/index/index.html +22 -0
- data/lib/subtrac/templates/projects/trac_theme/svn/trunk/templates/layout.html +56 -0
- data/lib/subtrac/templates/projects/trac_theme/svn/trunk/templates/site.html +27 -0
- data/lib/subtrac/templates/projects/trac_theme/svn/trunk/templates/theme.html +86 -0
- data/lib/subtrac/templates/projects/trac_theme/trac/wiki/WikiStart +4 -0
- data/lib/subtrac/templates/trac.erb +25 -0
- data/lib/subtrac/templates/vhost.erb +35 -0
- data/lib/subtrac/trac-plugins/advancedticketworkflowplugin/advancedworkflow/__init__.py +0 -0
- data/lib/subtrac/trac-plugins/advancedticketworkflowplugin/advancedworkflow/controller.py +419 -0
- data/lib/subtrac/trac-plugins/advancedticketworkflowplugin/setup.cfg +3 -0
- data/lib/subtrac/trac-plugins/advancedticketworkflowplugin/setup.py +20 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/__init__.py +0 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/action.py +28 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/action_email.py +168 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/action_zendesk_forum.py +137 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/admin.py +91 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/api.py +199 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/client.py +105 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/events.py +287 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/eventsadmin.py +71 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/htdocs/clients.css +4 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/model.py +135 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/processor.py +70 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/reportmanager.py +142 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/reports.py +231 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/summary.py +27 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/summary_milestone.py +152 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/summary_ticketchanges.py +160 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/templates/admin_client_events.html +124 -0
- data/lib/subtrac/trac-plugins/clientsplugin/clients/templates/admin_clients.html +134 -0
- data/lib/subtrac/trac-plugins/clientsplugin/cron/changes.xslt +132 -0
- data/lib/subtrac/trac-plugins/clientsplugin/cron/run-client-event +97 -0
- data/lib/subtrac/trac-plugins/clientsplugin/cron/summary.xslt +161 -0
- data/lib/subtrac/trac-plugins/clientsplugin/setup.py +43 -0
- data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/__init__.py +4 -0
- data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/burndownchart.py +273 -0
- data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/hoursinplaceeditor.py +44 -0
- data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/hoursremaining.py +36 -0
- data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/htdocs/jquery-1.2.3.min.js +32 -0
- data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/htdocs/jquery.jeditable.js +409 -0
- data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/htdocs/jquery.jeditable.mini.js +30 -0
- data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/templates/edithours.html +53 -0
- data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/tests/burndownchart.py +181 -0
- data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/tests/hoursremaining.py +66 -0
- data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/tests/workloadchart.py +47 -0
- data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/utils.py +93 -0
- data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/workloadchart.py +86 -0
- data/lib/subtrac/trac-plugins/estimationtoolsplugin/setup.py +20 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/SumRollups.js +23 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/adw_tracdb.py +128 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/git-post-receive +40 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/trac-post-commit.py +285 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/trac_billing.py +173 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/utils/__init__.py +0 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/utils/mail.py +164 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/setup.py +69 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/__init__.py +1 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/api.py +292 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/blackmagic.py +172 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/dbhelper.py +178 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/billingplugin.css +25 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/field_disabler.js +6 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/formatDate.js +356 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/js/tip_centerwindow.js +100 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/js/tip_followscroll.js +84 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/js/wz_tooltip.js +1149 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/linkifyer.js +119 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/query.js +73 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/ticket.js +165 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/query_webui.py +28 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/reportmanager.py +221 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/reports.py +675 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/reports_filter.py +150 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/statuses.py +25 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/tande_filters.py +131 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/templates/billing.cs +84 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/templates/billing.html +104 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/ticket_daemon.py +194 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/ticket_policy.py +62 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/ticket_webui.py +28 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/usermanual.py +127 -0
- data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/webui.py +129 -0
- data/lib/subtrac/trac-plugins/worklogplugin/setup.py +29 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/__init__.py +1 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/api.py +187 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/jqModal.css +40 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/jqModal.js +67 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/jquery.mousewheel.pack.js +12 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/jquery.timeentry.pack.js +7 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/tracWorklog.js +40 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/ui.datepicker.css +208 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/ui.datepicker.js +1439 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/work.png +0 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/work.xcf +0 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/worklogplugin.css +80 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/workstart.png +0 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/workstop.png +0 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/manager.py +336 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/reports.py +598 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/templates/worklog.html +45 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/templates/worklog_stop.html +70 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/templates/worklog_user.html +40 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/templates/worklog_webadminui.html +59 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/ticket_daemon.py +33 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/ticket_filter.py +153 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/timeline_hook.py +96 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/usermanual.py +29 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/util.py +31 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/webadminui.py +47 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/webui.py +174 -0
- data/lib/subtrac/trac-plugins/worklogplugin/worklog/xmlrpc.py +73 -0
- data/lib/subtrac/version.rb +4 -0
- metadata +191 -0
@@ -0,0 +1,199 @@
|
|
1
|
+
import time
|
2
|
+
from trac.core import *
|
3
|
+
from trac.env import IEnvironmentSetupParticipant
|
4
|
+
from reportmanager import CustomReportManager
|
5
|
+
|
6
|
+
class ClientsSetupParticipant(Component):
|
7
|
+
implements(IEnvironmentSetupParticipant)
|
8
|
+
|
9
|
+
db_version_key = None
|
10
|
+
db_version = None
|
11
|
+
db_installed_version = None
|
12
|
+
|
13
|
+
def __init__(self):
|
14
|
+
self.db_version_key = 'clients_plugin_version'
|
15
|
+
self.db_version = 5
|
16
|
+
self.db_installed_version = None
|
17
|
+
|
18
|
+
# Initialise database schema version tracking.
|
19
|
+
db = self.env.get_db_cnx()
|
20
|
+
cursor = db.cursor()
|
21
|
+
cursor.execute("SELECT value FROM system WHERE name=%s", (self.db_version_key,))
|
22
|
+
try:
|
23
|
+
self.db_installed_version = int(cursor.fetchone()[0])
|
24
|
+
except:
|
25
|
+
self.db_installed_version = 0
|
26
|
+
cursor.execute("INSERT INTO system (name,value) VALUES(%s,%s)",
|
27
|
+
(self.db_version_key, self.db_installed_version))
|
28
|
+
db.commit()
|
29
|
+
db.close()
|
30
|
+
|
31
|
+
def system_needs_upgrade(self):
|
32
|
+
return self.db_installed_version < self.db_version
|
33
|
+
|
34
|
+
def do_db_upgrade(self):
|
35
|
+
db = self.env.get_db_cnx()
|
36
|
+
cursor = db.cursor()
|
37
|
+
|
38
|
+
# Do the staged updates
|
39
|
+
try:
|
40
|
+
if self.db_installed_version < 2:
|
41
|
+
print 'Creating client table'
|
42
|
+
cursor.execute('CREATE TABLE client ('
|
43
|
+
'name TEXT,'
|
44
|
+
'description TEXT,'
|
45
|
+
'changes_list TEXT,'
|
46
|
+
'changes_period TEXT,'
|
47
|
+
'changes_lastupdate INTEGER,'
|
48
|
+
'summary_list TEXT,'
|
49
|
+
'summary_period TEXT,'
|
50
|
+
'summary_lastupdate INTEGER'
|
51
|
+
')')
|
52
|
+
# Import old Enums
|
53
|
+
cursor.execute('INSERT INTO client (name) '
|
54
|
+
'SELECT name FROM enum WHERE type=%s', ('client',))
|
55
|
+
# Clean them out
|
56
|
+
cursor.execute('DELETE FROM enum WHERE type=%s', ('client',))
|
57
|
+
|
58
|
+
if self.db_installed_version < 3:
|
59
|
+
print 'Updating clients table (v3)'
|
60
|
+
cursor.execute('ALTER TABLE client '
|
61
|
+
'ADD COLUMN default_rate INTEGER')
|
62
|
+
cursor.execute('ALTER TABLE client '
|
63
|
+
'ADD COLUMN currency TEXT')
|
64
|
+
|
65
|
+
if self.db_installed_version < 4:
|
66
|
+
print 'Updating clients table (v4)'
|
67
|
+
cursor.execute('CREATE TABLE client_events ('
|
68
|
+
'name TEXT,'
|
69
|
+
'summary TEXT,'
|
70
|
+
'action TEXT,'
|
71
|
+
'lastrun INTEGER'
|
72
|
+
')')
|
73
|
+
cursor.execute('CREATE TABLE client_event_summary_options ('
|
74
|
+
'client_event TEXT,'
|
75
|
+
'client TEXT,'
|
76
|
+
'name TEXT,'
|
77
|
+
'value TEXT'
|
78
|
+
')')
|
79
|
+
cursor.execute('CREATE TABLE client_event_action_options ('
|
80
|
+
'client_event TEXT,'
|
81
|
+
'client TEXT,'
|
82
|
+
'name TEXT,'
|
83
|
+
'value TEXT'
|
84
|
+
')')
|
85
|
+
|
86
|
+
if self.db_installed_version < 5:
|
87
|
+
print 'Updating clients table (v5)'
|
88
|
+
cursor.execute('INSERT INTO client_events '
|
89
|
+
'SELECT "Weekly Summary", "Milestone Summary", "Send Email", MAX(summary_lastupdate) '
|
90
|
+
'FROM client')
|
91
|
+
cursor.execute('INSERT INTO client_event_action_options '
|
92
|
+
'SELECT "Weekly Summary", name, "Email Addresses", summary_list '
|
93
|
+
'FROM client '
|
94
|
+
'WHERE summary_list!=""')
|
95
|
+
cursor.execute('INSERT INTO client_events '
|
96
|
+
'SELECT "Ticket Changes", "Ticket Change Summary", "Send Email", MAX(changes_lastupdate) '
|
97
|
+
'FROM client')
|
98
|
+
cursor.execute('INSERT INTO client_event_action_options '
|
99
|
+
'SELECT "Ticket Changes", name, "Email Addresses", changes_list '
|
100
|
+
'FROM client '
|
101
|
+
'WHERE changes_list!=""')
|
102
|
+
cursor.execute('CREATE TEMPORARY TABLE client_tmp ('
|
103
|
+
'name TEXT,'
|
104
|
+
'description TEXT,'
|
105
|
+
'default_rate INTEGER,'
|
106
|
+
'currency TEXT'
|
107
|
+
')')
|
108
|
+
cursor.execute('INSERT INTO client_tmp SELECT name, description, default_rate, currency FROM client')
|
109
|
+
cursor.execute('DROP TABLE client')
|
110
|
+
cursor.execute('CREATE TABLE client ('
|
111
|
+
'name TEXT,'
|
112
|
+
'description TEXT,'
|
113
|
+
'default_rate INTEGER,'
|
114
|
+
'currency TEXT'
|
115
|
+
')')
|
116
|
+
cursor.execute('INSERT INTO client SELECT name, description, default_rate, currency FROM client_tmp')
|
117
|
+
cursor.execute('DROP TABLE client_tmp')
|
118
|
+
|
119
|
+
#if self.db_installed_version < 6:
|
120
|
+
# print 'Updating clients table (v6)'
|
121
|
+
# cursor.execute('...')
|
122
|
+
|
123
|
+
# Updates complete, set the version
|
124
|
+
cursor.execute("UPDATE system SET value=%s WHERE name=%s",
|
125
|
+
(self.db_version, self.db_version_key))
|
126
|
+
db.commit()
|
127
|
+
db.close()
|
128
|
+
except Exception, e:
|
129
|
+
self.log.error("WorklogPlugin Exception: %s" % (e,));
|
130
|
+
db.rollback()
|
131
|
+
|
132
|
+
def do_reports_upgrade(self):
|
133
|
+
mgr = CustomReportManager(self.env, self.log)
|
134
|
+
r = __import__('reports', globals(), locals(), ['reports'])
|
135
|
+
|
136
|
+
for report_group in r.reports:
|
137
|
+
rlist = report_group['reports']
|
138
|
+
group_title = report_group['title']
|
139
|
+
for report in rlist:
|
140
|
+
title = report['title']
|
141
|
+
new_version = report['version']
|
142
|
+
mgr.add_report(report["title"], 'Clients Plugin', \
|
143
|
+
report['description'], report['sql'], \
|
144
|
+
report['uuid'], report['version'],
|
145
|
+
'Timing and Estimation Plugin',
|
146
|
+
group_title)
|
147
|
+
|
148
|
+
def ticket_fields_need_upgrade(self):
|
149
|
+
section = 'ticket-custom'
|
150
|
+
return ('text' != self.config.get(section, 'client') \
|
151
|
+
or 'text' != self.config.get(section, 'clientrate'))
|
152
|
+
|
153
|
+
def do_ticket_field_upgrade(self):
|
154
|
+
section = 'ticket-custom'
|
155
|
+
|
156
|
+
self.config.set(section,'client', 'text')
|
157
|
+
self.config.set(section,'client.label', 'Client')
|
158
|
+
|
159
|
+
self.config.set(section,'clientrate', 'text')
|
160
|
+
self.config.set(section,'clientrate.label', 'Client Charge Rate')
|
161
|
+
|
162
|
+
self.config.save();
|
163
|
+
|
164
|
+
|
165
|
+
# IEnvironmentSetupParticipant methods
|
166
|
+
def environment_created(self):
|
167
|
+
"""Called when a new Trac environment is created."""
|
168
|
+
if self.environment_needs_upgrade(None):
|
169
|
+
self.upgrade_environment(None)
|
170
|
+
|
171
|
+
def environment_needs_upgrade(self, db):
|
172
|
+
"""Called when Trac checks whether the environment needs to be upgraded.
|
173
|
+
|
174
|
+
Should return `True` if this participant needs an upgrade to be
|
175
|
+
performed, `False` otherwise.
|
176
|
+
|
177
|
+
"""
|
178
|
+
return (self.system_needs_upgrade() \
|
179
|
+
or self.ticket_fields_need_upgrade())
|
180
|
+
|
181
|
+
def upgrade_environment(self, db):
|
182
|
+
"""Actually perform an environment upgrade.
|
183
|
+
|
184
|
+
Implementations of this method should not commit any database
|
185
|
+
transactions. This is done implicitly after all participants have
|
186
|
+
performed the upgrades they need without an error being raised.
|
187
|
+
"""
|
188
|
+
print 'ClientsPlugin needs an upgrade'
|
189
|
+
print ' * Upgrading db'
|
190
|
+
self.do_db_upgrade()
|
191
|
+
print ' * Upgrading reports'
|
192
|
+
self.do_reports_upgrade()
|
193
|
+
|
194
|
+
if self.ticket_fields_need_upgrade():
|
195
|
+
print ' * Upgrading fields'
|
196
|
+
self.do_ticket_field_upgrade()
|
197
|
+
|
198
|
+
print 'Done Upgrading'
|
199
|
+
|
@@ -0,0 +1,105 @@
|
|
1
|
+
from trac.core import *
|
2
|
+
from trac.web.chrome import add_stylesheet, ITemplateProvider
|
3
|
+
from trac.web.api import IRequestFilter, ITemplateStreamFilter
|
4
|
+
from trac.ticket.api import ITicketManipulator
|
5
|
+
from trac.ticket.model import Ticket
|
6
|
+
from trac.util.html import html, Markup
|
7
|
+
|
8
|
+
from genshi.core import Markup
|
9
|
+
from genshi.builder import tag
|
10
|
+
from genshi.filters.transform import Transformer
|
11
|
+
|
12
|
+
from clients import model
|
13
|
+
from StringIO import StringIO
|
14
|
+
|
15
|
+
class ClientModule(Component):
|
16
|
+
"""Allows for tickets to be assigned to particular clients."""
|
17
|
+
|
18
|
+
implements(IRequestFilter, ITemplateStreamFilter, ITemplateProvider)
|
19
|
+
|
20
|
+
# IRequestFilter methods
|
21
|
+
def pre_process_request(self, req, handler):
|
22
|
+
# Add Stylesheet here, so that the ticket page gets it too :)
|
23
|
+
add_stylesheet(req, "clients/clients.css")
|
24
|
+
return handler
|
25
|
+
|
26
|
+
def post_process_request(self, req, template, data, content_type):
|
27
|
+
# Some ticket views do not actually load the data in this way
|
28
|
+
# e.g. action=history or action=diff
|
29
|
+
if not data or not data.has_key('fields'):
|
30
|
+
return template, data, content_type
|
31
|
+
|
32
|
+
newticket = req.path_info.startswith('/newticket')
|
33
|
+
if req.path_info.startswith('/ticket/') or newticket:
|
34
|
+
for field in data['fields']:
|
35
|
+
if 'client' == field['name']:
|
36
|
+
field['type'] = 'select'
|
37
|
+
field['options'] = []
|
38
|
+
for client in model.Client.select(self.env):
|
39
|
+
field['options'].append(client.name)
|
40
|
+
if newticket:
|
41
|
+
default_client = self.config.get('ticket', 'default_client')
|
42
|
+
if default_client:
|
43
|
+
data['ticket']['client'] = default_client
|
44
|
+
break;
|
45
|
+
elif req.path_info.startswith('/query'):
|
46
|
+
if data['fields'].has_key('client'):
|
47
|
+
data['fields']['client']['type'] = 'select'
|
48
|
+
data['fields']['client']['options'] = []
|
49
|
+
for client in model.Client.select(self.env):
|
50
|
+
data['fields']['client']['options'].append(client.name)
|
51
|
+
return template, data, content_type
|
52
|
+
|
53
|
+
# ITemplateStreamFilter methods
|
54
|
+
def filter_stream(self, req, method, filename, stream, data):
|
55
|
+
newticket = req.path_info.startswith('/newticket')
|
56
|
+
if req.path_info.startswith('/ticket/') or newticket:
|
57
|
+
setdefaultrate = ''
|
58
|
+
if newticket:
|
59
|
+
setdefaultrate = "$('#field-client').trigger('change');"
|
60
|
+
|
61
|
+
script = StringIO()
|
62
|
+
script.write("""
|
63
|
+
$(document).ready(function() {
|
64
|
+
$('#field-client').change(function() {
|
65
|
+
""");
|
66
|
+
script.write('var clientrates = new Array();')
|
67
|
+
for client in model.Client.select(self.env):
|
68
|
+
script.write('clientrates["%s"] = %s;' % (client.name, client.default_rate or '""'))
|
69
|
+
script.write("""
|
70
|
+
try { $('#field-clientrate').attr('value', clientrates[this.options[this.selectedIndex].value]); }
|
71
|
+
catch (er) { }
|
72
|
+
});
|
73
|
+
%s
|
74
|
+
});""" % setdefaultrate);
|
75
|
+
stream |= Transformer('.//head').append(tag.script(script.getvalue(), type_='text/javascript'))
|
76
|
+
return stream
|
77
|
+
|
78
|
+
# ITemplateProvider
|
79
|
+
def get_htdocs_dirs(self):
|
80
|
+
"""Return the absolute path of a directory containing additional
|
81
|
+
static resources (such as images, style sheets, etc).
|
82
|
+
"""
|
83
|
+
from pkg_resources import resource_filename
|
84
|
+
return [('clients', resource_filename(__name__, 'htdocs'))]
|
85
|
+
|
86
|
+
def get_templates_dirs(self):
|
87
|
+
"""Return the absolute path of the directory containing the provided
|
88
|
+
ClearSilver templates.
|
89
|
+
"""
|
90
|
+
from pkg_resources import resource_filename
|
91
|
+
return [resource_filename(__name__, 'templates')]
|
92
|
+
|
93
|
+
# ITicketManipulator methods
|
94
|
+
def prepare_ticket(self, req, ticket, fields, actions):
|
95
|
+
pass
|
96
|
+
|
97
|
+
def validate_ticket(self, req, ticket):
|
98
|
+
# Todo validate client is valid
|
99
|
+
pass
|
100
|
+
#if req.args.get('action') == 'resolve':
|
101
|
+
# links = TicketLinks(self.env, ticket)
|
102
|
+
# for i in links.blocked_by:
|
103
|
+
# if Ticket(self.env, i)['status'] != 'closed':
|
104
|
+
# yield None, 'Ticket #%s is blocking this ticket'%i
|
105
|
+
|
@@ -0,0 +1,287 @@
|
|
1
|
+
import md5
|
2
|
+
import sys
|
3
|
+
import time
|
4
|
+
|
5
|
+
from trac.core import *
|
6
|
+
|
7
|
+
from clients.summary import IClientSummaryProvider
|
8
|
+
from clients.action import IClientActionProvider
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
__all__ = ['ClientEvent']
|
13
|
+
|
14
|
+
def simplify_whitespace(name):
|
15
|
+
"""Strip spaces and remove duplicate spaces within names"""
|
16
|
+
return ' '.join(name.split())
|
17
|
+
|
18
|
+
class ClientEventsSystem(Component):
|
19
|
+
summaries = ExtensionPoint(IClientSummaryProvider)
|
20
|
+
actions = ExtensionPoint(IClientActionProvider)
|
21
|
+
|
22
|
+
def get_summaries(self):
|
23
|
+
for summary in self.summaries:
|
24
|
+
yield summary.get_name()
|
25
|
+
|
26
|
+
def get_summary(self, name):
|
27
|
+
for summary in self.summaries:
|
28
|
+
if name == summary.get_name():
|
29
|
+
return summary
|
30
|
+
return None
|
31
|
+
|
32
|
+
def get_actions(self):
|
33
|
+
for action in self.actions:
|
34
|
+
yield action.get_name()
|
35
|
+
|
36
|
+
def get_action(self, name):
|
37
|
+
for action in self.actions:
|
38
|
+
if name == action.get_name():
|
39
|
+
return action
|
40
|
+
return None
|
41
|
+
|
42
|
+
class ClientEvent(object):
|
43
|
+
|
44
|
+
def __init__(self, env, name=None, client=None, db=None):
|
45
|
+
self.env = env
|
46
|
+
if name:
|
47
|
+
name = simplify_whitespace(name)
|
48
|
+
if name:
|
49
|
+
if not db:
|
50
|
+
db = self.env.get_db_cnx()
|
51
|
+
cursor = db.cursor()
|
52
|
+
cursor.execute("SELECT summary, action, lastrun "
|
53
|
+
"FROM client_events "
|
54
|
+
"WHERE name=%s", (name,))
|
55
|
+
row = cursor.fetchone()
|
56
|
+
if not row:
|
57
|
+
raise TracError('Client Event %s does not exist.' % name)
|
58
|
+
self.md5 = md5.new(name).hexdigest()
|
59
|
+
self.name = self._old_name = name
|
60
|
+
self.summary = row[0] or ''
|
61
|
+
self.action = row[1] or ''
|
62
|
+
self.lastrun = row[2] or 0
|
63
|
+
self._load_options(client, db)
|
64
|
+
else:
|
65
|
+
self.name = self._old_name = None
|
66
|
+
self.summary = ''
|
67
|
+
self.action = ''
|
68
|
+
self.lastrun = 0
|
69
|
+
|
70
|
+
|
71
|
+
exists = property(fget=lambda self: self._old_name is not None)
|
72
|
+
|
73
|
+
|
74
|
+
def delete(self, db=None):
|
75
|
+
assert self.exists, 'Cannot deleting non-existent client event'
|
76
|
+
if not db:
|
77
|
+
db = self.env.get_db_cnx()
|
78
|
+
handle_ta = True
|
79
|
+
else:
|
80
|
+
handle_ta = False
|
81
|
+
|
82
|
+
cursor = db.cursor()
|
83
|
+
self.env.log.info('Deleting client event %s' % self.name)
|
84
|
+
cursor.execute("DELETE FROM client_events WHERE name=%s", (self.name,))
|
85
|
+
|
86
|
+
self.name = self._old_name = None
|
87
|
+
|
88
|
+
if handle_ta:
|
89
|
+
db.commit()
|
90
|
+
|
91
|
+
def insert(self, db=None):
|
92
|
+
assert not self.exists, 'Cannot insert existing client event'
|
93
|
+
system = ClientEventsSystem(self.env);
|
94
|
+
assert system.get_summary(self.summary) is not None , 'Invalid summary'
|
95
|
+
assert system.get_action(self.action) is not None, 'Invalid action'
|
96
|
+
self.name = simplify_whitespace(self.name)
|
97
|
+
assert self.name, 'Cannot create client event with no name'
|
98
|
+
if not db:
|
99
|
+
db = self.env.get_db_cnx()
|
100
|
+
handle_ta = True
|
101
|
+
else:
|
102
|
+
handle_ta = False
|
103
|
+
|
104
|
+
# Todo: verify client_event with that name does not currently exist...
|
105
|
+
cursor = db.cursor()
|
106
|
+
self.env.log.debug("Creating new client event '%s'" % self.name)
|
107
|
+
cursor.execute("INSERT INTO client_events (name,summary,action,lastrun) "
|
108
|
+
"VALUES (%s,%s,%s,%s)",
|
109
|
+
(self.name, self.summary, self.action, int(time.time())))
|
110
|
+
|
111
|
+
if handle_ta:
|
112
|
+
db.commit()
|
113
|
+
|
114
|
+
def _load_client_options(self, client, opttype, db):
|
115
|
+
assert self.exists, 'Cannot update non-existent client event'
|
116
|
+
assert opttype in ('summary', 'action'), 'Invalid options type'
|
117
|
+
system = ClientEventsSystem(self.env);
|
118
|
+
if 'summary' == opttype:
|
119
|
+
thing = system.get_summary(self.summary)
|
120
|
+
assert thing is not None , 'Invalid summary'
|
121
|
+
self.summary_description = thing.get_description()
|
122
|
+
else:
|
123
|
+
thing = system.get_action(self.action)
|
124
|
+
assert thing is not None, 'Invalid action'
|
125
|
+
self.action_description = thing.get_description()
|
126
|
+
|
127
|
+
options = {}
|
128
|
+
table = 'client_event_' + opttype + '_options'
|
129
|
+
cursor = db.cursor()
|
130
|
+
cursor.execute("SELECT name, value "
|
131
|
+
"FROM " + table + " "
|
132
|
+
"WHERE client_event=%s AND client=%s",
|
133
|
+
(self._old_name, client or ''))
|
134
|
+
for name, value in cursor:
|
135
|
+
options[name] = value
|
136
|
+
rv = {}
|
137
|
+
for option in thing.options(client):
|
138
|
+
option['md5'] = md5.new(option['name']).hexdigest()
|
139
|
+
if options.has_key(option['name']):
|
140
|
+
option['value'] = options[option['name']]
|
141
|
+
else:
|
142
|
+
option['value'] = ''
|
143
|
+
rv[option['name']] = option
|
144
|
+
return rv
|
145
|
+
|
146
|
+
|
147
|
+
def _load_options(self, client, db):
|
148
|
+
self.summary_options = self._load_client_options(None, 'summary', db)
|
149
|
+
self.action_options = self._load_client_options(None, 'action', db)
|
150
|
+
if client:
|
151
|
+
self.summary_client_options = self._load_client_options(client, 'summary', db)
|
152
|
+
self.action_client_options = self._load_client_options(client, 'action', db)
|
153
|
+
|
154
|
+
|
155
|
+
def _update_client_options(self, client, opttype, options, db=None):
|
156
|
+
assert self.exists, 'Cannot update non-existent client event'
|
157
|
+
assert opttype in ('summary', 'action'), 'Invalid options type'
|
158
|
+
system = ClientEventsSystem(self.env);
|
159
|
+
if 'summary' == opttype:
|
160
|
+
thing = system.get_summary(self.summary)
|
161
|
+
assert thing is not None , 'Invalid summary'
|
162
|
+
else:
|
163
|
+
thing = system.get_action(self.action)
|
164
|
+
assert thing is not None, 'Invalid action'
|
165
|
+
|
166
|
+
if not db:
|
167
|
+
db = self.env.get_db_cnx()
|
168
|
+
handle_ta = True
|
169
|
+
else:
|
170
|
+
handle_ta = False
|
171
|
+
|
172
|
+
table = 'client_event_' + opttype + '_options'
|
173
|
+
cursor = db.cursor()
|
174
|
+
self.env.log.info('Updating client event "%s"' % self._old_name)
|
175
|
+
cursor.execute("DELETE FROM " + table + " "
|
176
|
+
"WHERE client_event=%s AND client=%s",
|
177
|
+
(self._old_name, client or ''))
|
178
|
+
|
179
|
+
valid_options = []
|
180
|
+
for option in thing.options(client):
|
181
|
+
valid_options.append(option['name'])
|
182
|
+
for option in options.values():
|
183
|
+
if option['name'] in valid_options:
|
184
|
+
cursor.execute("INSERT INTO " + table + " (client_event, client, name, value) "
|
185
|
+
"VALUES (%s, %s, %s, %s)",
|
186
|
+
(self._old_name, client or '', option['name'], option['value']))
|
187
|
+
|
188
|
+
if handle_ta:
|
189
|
+
db.commit()
|
190
|
+
|
191
|
+
|
192
|
+
def update_options(self, client=None, db=None):
|
193
|
+
assert self.exists, 'Cannot update non-existent client event'
|
194
|
+
if not db:
|
195
|
+
db = self.env.get_db_cnx()
|
196
|
+
handle_ta = True
|
197
|
+
else:
|
198
|
+
handle_ta = False
|
199
|
+
|
200
|
+
if client:
|
201
|
+
self._update_client_options(client, 'summary', self.summary_client_options, db)
|
202
|
+
self._update_client_options(client, 'action', self.action_client_options, db)
|
203
|
+
else:
|
204
|
+
self._update_client_options(None, 'summary', self.summary_options, db)
|
205
|
+
self._update_client_options(None, 'action', self.action_options, db)
|
206
|
+
|
207
|
+
if handle_ta:
|
208
|
+
db.commit()
|
209
|
+
|
210
|
+
|
211
|
+
def update(self, db=None):
|
212
|
+
assert self.exists, 'Cannot update non-existent client event'
|
213
|
+
self.name = simplify_whitespace(self.name)
|
214
|
+
assert self.name, 'Cannot update client event with no name'
|
215
|
+
if not db:
|
216
|
+
db = self.env.get_db_cnx()
|
217
|
+
handle_ta = True
|
218
|
+
else:
|
219
|
+
handle_ta = False
|
220
|
+
|
221
|
+
cursor = db.cursor()
|
222
|
+
self.env.log.info('Updating client event "%s"' % self._old_name)
|
223
|
+
cursor.execute("UPDATE client_events SET lastrun=%s "
|
224
|
+
"WHERE name=%s",
|
225
|
+
(int(self.lastrun), self._old_name))
|
226
|
+
|
227
|
+
if handle_ta:
|
228
|
+
db.commit()
|
229
|
+
|
230
|
+
def trigger(self, req, client, fromdate, todate, db=None):
|
231
|
+
assert self.exists, 'Cannot trigger a client event that does not exits'
|
232
|
+
system = ClientEventsSystem(self.env);
|
233
|
+
summary = system.get_summary(self.summary)
|
234
|
+
assert summary is not None , 'Invalid summary'
|
235
|
+
action = system.get_action(self.action)
|
236
|
+
assert action is not None, 'Invalid action'
|
237
|
+
|
238
|
+
if not summary.init(self, client):
|
239
|
+
self.env.log.info("Could not init summary")
|
240
|
+
return False
|
241
|
+
if not action.init(self, client):
|
242
|
+
self.env.log.info("Could not init action")
|
243
|
+
return False
|
244
|
+
|
245
|
+
self.env.log.debug("Performing action")
|
246
|
+
return action.perform(req, summary.get_summary(req, fromdate, todate))
|
247
|
+
|
248
|
+
|
249
|
+
def select(cls, env, client=None, db=None):
|
250
|
+
if not db:
|
251
|
+
db = env.get_db_cnx()
|
252
|
+
cursor = db.cursor()
|
253
|
+
cursor.execute("SELECT name, summary, action, lastrun "
|
254
|
+
"FROM client_events "
|
255
|
+
"ORDER BY name")
|
256
|
+
for name, summary, action, lastrun in cursor:
|
257
|
+
clev = cls(env)
|
258
|
+
clev.md5 = md5.new(name).hexdigest()
|
259
|
+
clev.name = clev._old_name = name
|
260
|
+
clev.summary = summary or ''
|
261
|
+
clev.action = action or ''
|
262
|
+
clev.lastrun = lastrun or 0
|
263
|
+
clev._load_options(client, db)
|
264
|
+
yield clev
|
265
|
+
select = classmethod(select)
|
266
|
+
|
267
|
+
def triggerall(cls, env, req, event, db=None):
|
268
|
+
if not db:
|
269
|
+
db = env.get_db_cnx()
|
270
|
+
|
271
|
+
try:
|
272
|
+
ev = cls(env, event, None, db)
|
273
|
+
except:
|
274
|
+
env.log.error("Could not run the event %s" % (event,))
|
275
|
+
return
|
276
|
+
#ev.lastrun = 1
|
277
|
+
now = int(time.time())
|
278
|
+
|
279
|
+
cursor = db.cursor()
|
280
|
+
cursor.execute("SELECT name FROM client ORDER BY name")
|
281
|
+
for client, in cursor:
|
282
|
+
env.log.info("Running event for client: %s" % (client, ))
|
283
|
+
clev = cls(env, event, client)
|
284
|
+
clev.trigger(req, client, ev.lastrun, now)
|
285
|
+
ev.lastrun = now
|
286
|
+
ev.update()
|
287
|
+
triggerall = classmethod(triggerall)
|