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,27 @@
|
|
1
|
+
from lxml import etree
|
2
|
+
from trac.core import *
|
3
|
+
|
4
|
+
class IClientSummaryProvider(Interface):
|
5
|
+
"""Extension point interface for components that define their own way
|
6
|
+
to summarise a given client.
|
7
|
+
"""
|
8
|
+
|
9
|
+
def get_name():
|
10
|
+
"""Return the name of the summary (for use in UI)
|
11
|
+
"""
|
12
|
+
|
13
|
+
def get_description():
|
14
|
+
"""Return the description of the summary (for use in UI)
|
15
|
+
"""
|
16
|
+
|
17
|
+
def options(client=None):
|
18
|
+
"""Return a series of tupoles defining the options
|
19
|
+
"""
|
20
|
+
|
21
|
+
def init(event, client):
|
22
|
+
"""Initialise the summary for a specific instance and client combo
|
23
|
+
"""
|
24
|
+
|
25
|
+
def get_summary(req, fromdate = None, todate = None):
|
26
|
+
"""Get the summary. This must return an etree object
|
27
|
+
"""
|
@@ -0,0 +1,152 @@
|
|
1
|
+
import re
|
2
|
+
import os
|
3
|
+
import sys
|
4
|
+
import locale
|
5
|
+
import time
|
6
|
+
import codecs
|
7
|
+
from datetime import datetime
|
8
|
+
from optparse import OptionParser
|
9
|
+
from StringIO import StringIO
|
10
|
+
|
11
|
+
from trac.core import *
|
12
|
+
from trac.env import open_environment
|
13
|
+
from trac.util.datefmt import format_date, to_datetime
|
14
|
+
from trac.wiki import wiki_to_html
|
15
|
+
from genshi import escape
|
16
|
+
|
17
|
+
from lxml import etree
|
18
|
+
from clients.summary import IClientSummaryProvider
|
19
|
+
from clients.processor import extract_client_text
|
20
|
+
|
21
|
+
|
22
|
+
class ClientMilestoneSummary(Component):
|
23
|
+
implements(IClientSummaryProvider)
|
24
|
+
|
25
|
+
client = None
|
26
|
+
debug = False
|
27
|
+
|
28
|
+
def get_name(self):
|
29
|
+
return "Milestone Summary"
|
30
|
+
|
31
|
+
def get_description(self):
|
32
|
+
return "Provide a summary of all tickets within milestones that have completion dates set where at least one ticket for that client is not closed in that milestone"
|
33
|
+
|
34
|
+
def options(self, client=None):
|
35
|
+
return []
|
36
|
+
|
37
|
+
def init(self, event, client):
|
38
|
+
self.client = client
|
39
|
+
return True
|
40
|
+
|
41
|
+
def get_summary(self, req, fromdate = None, todate = None):
|
42
|
+
def myformat_date(dte):
|
43
|
+
if dte:
|
44
|
+
return format_date(dte, '%e %b %Y')
|
45
|
+
return 'No date set'
|
46
|
+
def myformat_hours(hrs):
|
47
|
+
from math import floor
|
48
|
+
if hrs:
|
49
|
+
hrs = float(hrs)
|
50
|
+
if 0 != hrs:
|
51
|
+
neg = False
|
52
|
+
if hrs < 0:
|
53
|
+
neg = True
|
54
|
+
hours *= -1
|
55
|
+
mins = floor((hrs - floor(hrs)) * 60)
|
56
|
+
str = ''
|
57
|
+
if neg:
|
58
|
+
str = '-'
|
59
|
+
if hrs:
|
60
|
+
str = "%s%sh" % (str, int(floor(hrs)))
|
61
|
+
if mins:
|
62
|
+
str = "%s %sm" % (str, int(mins))
|
63
|
+
return str;
|
64
|
+
return 'No estimate available'
|
65
|
+
|
66
|
+
client = self.client
|
67
|
+
xml = etree.Element('clientsplugin')
|
68
|
+
|
69
|
+
# Place basic client info here
|
70
|
+
xclient = etree.SubElement(xml, 'client')
|
71
|
+
etree.SubElement(xclient, 'name').text = client
|
72
|
+
if fromdate:
|
73
|
+
etree.SubElement(xclient, 'lastupdate').text = myformat_date(fromdate)
|
74
|
+
|
75
|
+
# Information about milestones
|
76
|
+
milestones = {}
|
77
|
+
xmilestones = etree.SubElement(xml, 'milestones')
|
78
|
+
|
79
|
+
db = self.env.get_db_cnx()
|
80
|
+
have_data = False
|
81
|
+
# Load in a summary of the client's tickets
|
82
|
+
sql = ("""\
|
83
|
+
SELECT t.id, t.summary, t.description, t.status, t.milestone,
|
84
|
+
m.due, m.completed, m.description AS mdesc,
|
85
|
+
tcust2.value AS estimatedhours
|
86
|
+
FROM ticket_custom AS tcust
|
87
|
+
INNER JOIN ticket AS t ON tcust.ticket=t.id
|
88
|
+
LEFT JOIN ticket_custom AS tcust2 ON t.id=tcust2.ticket AND tcust2.name='estimatedhours'
|
89
|
+
LEFT JOIN milestone m ON t.milestone=m.name
|
90
|
+
WHERE tcust.name = 'client'
|
91
|
+
AND tcust.value = %s
|
92
|
+
AND t.milestone IN (
|
93
|
+
SELECT DISTINCT st.milestone
|
94
|
+
FROM ticket_custom AS stcust
|
95
|
+
INNER JOIN ticket AS st ON stcust.ticket=st.id
|
96
|
+
INNER JOIN milestone AS sm ON st.milestone=sm.name
|
97
|
+
WHERE stcust.name = tcust.name
|
98
|
+
AND stcust.value = tcust.value
|
99
|
+
AND st.status != 'closed'
|
100
|
+
AND sm.due > 0)
|
101
|
+
""")
|
102
|
+
cur2 = db.cursor()
|
103
|
+
cur2.execute(sql, (client,))
|
104
|
+
xsummary = etree.SubElement(xml, 'summary')
|
105
|
+
for tid, summary, description, status, milestone, due, completed, mdescription, estimatedhours in cur2:
|
106
|
+
have_data = True
|
107
|
+
if milestone:
|
108
|
+
if not milestones.has_key(milestone):
|
109
|
+
xmilestone = etree.SubElement(xmilestones, 'milestone')
|
110
|
+
etree.SubElement(xmilestone, 'name').text = milestone
|
111
|
+
etree.SubElement(xmilestone, 'duetimestamp').text = str(due)
|
112
|
+
etree.SubElement(xmilestone, 'due').text = myformat_date(due)
|
113
|
+
if completed:
|
114
|
+
etree.SubElement(xmilestone, 'completed').text = myformat_date(completed)
|
115
|
+
if mdescription:
|
116
|
+
xmilestone.append(etree.XML('<description>%s</description>' % wiki_to_html(extract_client_text(mdescription), self.env, req)))
|
117
|
+
else:
|
118
|
+
etree.SubElement(xmilestone, 'description').text = ''
|
119
|
+
# Store for use
|
120
|
+
milestones[milestone] = { 'hours': 0, 'xml': xmilestone }
|
121
|
+
|
122
|
+
# Add hours to create a total.
|
123
|
+
if estimatedhours:
|
124
|
+
milestones[milestone]['hours'] += float(estimatedhours)
|
125
|
+
|
126
|
+
self.env.log.debug(" Summarising ticket #%s" % tid)
|
127
|
+
ticket = etree.SubElement(xsummary, 'ticket')
|
128
|
+
etree.SubElement(ticket, 'id').text = str(tid)
|
129
|
+
etree.SubElement(ticket, 'summary').text = summary
|
130
|
+
ticket.append(etree.XML('<description>%s</description>' % wiki_to_html(extract_client_text(description), self.env, req)))
|
131
|
+
etree.SubElement(ticket, 'status').text = status
|
132
|
+
etree.SubElement(ticket, 'milestone').text = milestone
|
133
|
+
# For conveneince, put the date here too (keeps the XSLTs simpler)
|
134
|
+
etree.SubElement(ticket, 'due').text = myformat_date(due)
|
135
|
+
if estimatedhours:
|
136
|
+
etree.SubElement(ticket, 'estimatedhours').text = myformat_hours(estimatedhours)
|
137
|
+
|
138
|
+
|
139
|
+
# Put the total hours into the milestone info
|
140
|
+
for milestone in milestones:
|
141
|
+
etree.SubElement(milestones[milestone]['xml'], 'estimatedhours').text = myformat_hours(milestones[milestone]['hours'])
|
142
|
+
|
143
|
+
if self.debug:
|
144
|
+
file = open('/tmp/send-client-email.xml', 'w')
|
145
|
+
file.write(etree.tostring(xml, pretty_print=True))
|
146
|
+
file.close()
|
147
|
+
self.env.log.debug(" Wrote XML to /tmp/send-client-email.xml")
|
148
|
+
|
149
|
+
if not have_data:
|
150
|
+
return None
|
151
|
+
|
152
|
+
return xml
|
@@ -0,0 +1,160 @@
|
|
1
|
+
import re
|
2
|
+
import os
|
3
|
+
import sys
|
4
|
+
import locale
|
5
|
+
import time
|
6
|
+
import codecs
|
7
|
+
from datetime import datetime
|
8
|
+
from optparse import OptionParser
|
9
|
+
from StringIO import StringIO
|
10
|
+
|
11
|
+
from trac.core import *
|
12
|
+
from trac.env import open_environment
|
13
|
+
from trac.util.datefmt import format_date, to_datetime
|
14
|
+
from trac.wiki import wiki_to_html
|
15
|
+
from genshi import escape
|
16
|
+
|
17
|
+
from lxml import etree
|
18
|
+
from clients.summary import IClientSummaryProvider
|
19
|
+
from clients.processor import extract_client_text
|
20
|
+
|
21
|
+
|
22
|
+
class ClientTicketChanges(Component):
|
23
|
+
implements(IClientSummaryProvider)
|
24
|
+
|
25
|
+
client = None
|
26
|
+
debug = False
|
27
|
+
|
28
|
+
def get_name(self):
|
29
|
+
return "Ticket Change Summary"
|
30
|
+
|
31
|
+
def get_description(self):
|
32
|
+
return "Provide a summary of ticket changes since the last run"
|
33
|
+
|
34
|
+
def options(self, client=None):
|
35
|
+
return []
|
36
|
+
|
37
|
+
def init(self, event, client):
|
38
|
+
self.client = client
|
39
|
+
return True
|
40
|
+
|
41
|
+
def get_summary(self, req, fromdate = None, todate = None):
|
42
|
+
def myformat_date(dte):
|
43
|
+
if dte:
|
44
|
+
return format_date(dte, '%e %b %Y')
|
45
|
+
return 'No date set'
|
46
|
+
def myformat_hours(hrs):
|
47
|
+
from math import floor
|
48
|
+
if hrs:
|
49
|
+
hrs = float(hrs)
|
50
|
+
if 0 != hrs:
|
51
|
+
neg = False
|
52
|
+
if hrs < 0:
|
53
|
+
neg = True
|
54
|
+
hours *= -1
|
55
|
+
mins = floor((hrs - floor(hrs)) * 60)
|
56
|
+
str = ''
|
57
|
+
if neg:
|
58
|
+
str = '-'
|
59
|
+
if hrs:
|
60
|
+
str = "%s%sh" % (str, int(floor(hrs)))
|
61
|
+
if mins:
|
62
|
+
str = "%s %sm" % (str, int(mins))
|
63
|
+
return str;
|
64
|
+
return 'No estimate available'
|
65
|
+
|
66
|
+
client = self.client
|
67
|
+
xml = etree.Element('clientsplugin')
|
68
|
+
|
69
|
+
# Place basic client info here
|
70
|
+
xclient = etree.SubElement(xml, 'client')
|
71
|
+
etree.SubElement(xclient, 'name').text = client
|
72
|
+
if fromdate:
|
73
|
+
etree.SubElement(xclient, 'lastupdate').text = myformat_date(fromdate)
|
74
|
+
|
75
|
+
db = self.env.get_db_cnx()
|
76
|
+
have_data = False
|
77
|
+
# Load in any changes that have happend
|
78
|
+
sql = ("""\
|
79
|
+
SELECT t.id, t.summary, t.description, t.status, t.resolution, t.milestone, m.due,
|
80
|
+
tchng.field, tchng.oldvalue, tchng.newvalue
|
81
|
+
FROM ticket_custom tcust
|
82
|
+
INNER JOIN ticket AS t ON tcust.ticket=t.id
|
83
|
+
INNER JOIN ticket_change AS tchng ON t.id=tchng.ticket
|
84
|
+
LEFT JOIN milestone AS m ON t.milestone=m.name
|
85
|
+
WHERE tcust.name = 'client'
|
86
|
+
AND tcust.value = %s
|
87
|
+
AND tchng.field IN ('comment', 'status', 'resolution', 'milestone')
|
88
|
+
AND tchng.time >= %s
|
89
|
+
AND tchng.time < %s
|
90
|
+
AND t.milestone IN (
|
91
|
+
SELECT DISTINCT st.milestone
|
92
|
+
FROM ticket_custom AS stcust
|
93
|
+
INNER JOIN ticket AS st ON stcust.ticket=st.id
|
94
|
+
INNER JOIN milestone AS sm ON st.milestone=sm.name
|
95
|
+
WHERE stcust.name = tcust.name
|
96
|
+
AND stcust.value = tcust.value
|
97
|
+
AND sm.due > 0)
|
98
|
+
ORDER BY t.time
|
99
|
+
""")
|
100
|
+
cur2 = db.cursor()
|
101
|
+
cur2.execute(sql, (client, fromdate, todate))
|
102
|
+
changes = etree.SubElement(xml, 'changes')
|
103
|
+
lasttid = 0
|
104
|
+
for tid, summary, description, status, resolution, milestone, due, cgfield, oldvalue, newvalue in cur2:
|
105
|
+
text = ''
|
106
|
+
if 'status' == cgfield:
|
107
|
+
text = 'Status changed from "%s" to "%s"' % (oldvalue, newvalue)
|
108
|
+
elif 'milestone' == cgfield:
|
109
|
+
text = 'Milestone changed from "%s" to "%s" - please check for revised delivery date.' % (oldvalue, newvalue)
|
110
|
+
elif 'resolution' == cgfield:
|
111
|
+
if oldvalue and not newvalue:
|
112
|
+
text = 'Resolution removed'
|
113
|
+
elif not oldvalue and newvalue:
|
114
|
+
text = 'Resolution set to "%s"' % (newvalue)
|
115
|
+
else:
|
116
|
+
text = 'Resolution changed from "%s" to "%s"' % (oldvalue, newvalue)
|
117
|
+
elif 'comment' == cgfield:
|
118
|
+
# Todo - extract...
|
119
|
+
text = extract_client_text(newvalue).strip()
|
120
|
+
if '' == text:
|
121
|
+
# No comments for the client here so ignore it.
|
122
|
+
continue
|
123
|
+
text = "''Comment for your information:''[[BR]][[BR]]" + text
|
124
|
+
else:
|
125
|
+
# Client should not know any more than this
|
126
|
+
continue
|
127
|
+
|
128
|
+
if self.debug:
|
129
|
+
self.env.log.debug(" Change notification (%s) for ticket #%s" % (cgfield, tid))
|
130
|
+
have_data = True
|
131
|
+
if lasttid != tid:
|
132
|
+
ticket = etree.SubElement(changes, 'ticket')
|
133
|
+
etree.SubElement(ticket, 'id').text = str(tid)
|
134
|
+
etree.SubElement(ticket, 'summary').text = summary
|
135
|
+
ticket.append(etree.XML('<description>%s</description>' % wiki_to_html(extract_client_text(description), self.env, req)))
|
136
|
+
etree.SubElement(ticket, 'status').text = status
|
137
|
+
etree.SubElement(ticket, 'resolution').text = resolution
|
138
|
+
etree.SubElement(ticket, 'milestone').text = milestone
|
139
|
+
etree.SubElement(ticket, 'due').text = myformat_date(due)
|
140
|
+
changelog = etree.SubElement(ticket, 'changelog')
|
141
|
+
|
142
|
+
detail = etree.XML('<detail>%s</detail>' % wiki_to_html(text, self.env, req))
|
143
|
+
detail.set('field', cgfield)
|
144
|
+
if oldvalue:
|
145
|
+
detail.set('oldvalue', oldvalue)
|
146
|
+
if newvalue:
|
147
|
+
detail.set('newvalue', newvalue)
|
148
|
+
changelog.append(detail)
|
149
|
+
lasttid = tid
|
150
|
+
|
151
|
+
if self.debug:
|
152
|
+
file = open('/tmp/send-client-email.xml', 'w')
|
153
|
+
file.write(etree.tostring(xml, pretty_print=True))
|
154
|
+
file.close()
|
155
|
+
self.env.log.debug(" Wrote XML to /tmp/send-client-email.xml")
|
156
|
+
|
157
|
+
if not have_data:
|
158
|
+
return None
|
159
|
+
|
160
|
+
return xml
|
@@ -0,0 +1,124 @@
|
|
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
|
+
<xi:include href="admin.html" />
|
8
|
+
<head>
|
9
|
+
<title>Client Events</title>
|
10
|
+
<style type="text/css">
|
11
|
+
p.contextual { margin-top:0; margin-bottom:2em }
|
12
|
+
</style>
|
13
|
+
</head>
|
14
|
+
|
15
|
+
<body>
|
16
|
+
<h2>Manage Client Events</h2>
|
17
|
+
|
18
|
+
<py:choose test="view">
|
19
|
+
<form py:when="'detail'" class="mod" id="modclient" method="post">
|
20
|
+
<fieldset>
|
21
|
+
<legend>Event Overview:</legend>
|
22
|
+
<div class="field">
|
23
|
+
<label><b>Name:</b> $event.name</label>
|
24
|
+
<p class="help">Run this event using the <a href="http://trac-hacks.org/browser/clientsplugin/0.11/cron/run-client-event">run-client-event</a> script with the argument: -c "$event.name"</p>
|
25
|
+
</div>
|
26
|
+
<div class="field">
|
27
|
+
<label><b>Summary:</b> $event.summary</label>
|
28
|
+
<p class="help">$event.summary_description</p>
|
29
|
+
</div>
|
30
|
+
<div class="field">
|
31
|
+
<label><b>Action:</b> $event.action</label>
|
32
|
+
<p class="help">$event.action_description</p>
|
33
|
+
</div>
|
34
|
+
<div class="field">
|
35
|
+
<label><b>Last run:</b> ${pretty_timedelta(event.lastrun)} (${format_date(event.lastrun)} ${format_time(event.lastrun, str('%H:%M'))})</label>
|
36
|
+
</div>
|
37
|
+
</fieldset>
|
38
|
+
<py:for each="options in (event.summary_options, event.action_options)">
|
39
|
+
<fieldset py:if="options">
|
40
|
+
<py:with vars="field = options==event.summary_options and 'summary' or 'action'">
|
41
|
+
<legend py:if="'summary'==field">Summary Options</legend>
|
42
|
+
<legend py:if="'action'==field">Action Options</legend>
|
43
|
+
<div py:for="option in options.values()" class="field">
|
44
|
+
<label>$option.name</label><br />
|
45
|
+
<py:choose test="option.type">
|
46
|
+
<textarea py:when="'large'" name="${field}-option-${option.md5}" rows="10" cols="70">$option.value</textarea>
|
47
|
+
<input py:when="'small'" name="${field}-option-${option.md5}" type="text" size="5" value="$option.value" />
|
48
|
+
<select py:when="'list'" name="${field}-option-${option.md5}">
|
49
|
+
<option py:for="val in option.vals" selected="${val == option.value and 'selected' or None}">$val</option>
|
50
|
+
</select>
|
51
|
+
<input py:otherwise="" name="${field}-option-${option.md5}" type="text" size="60" value="$option.value" />
|
52
|
+
</py:choose>
|
53
|
+
<p class="help">$option.description</p>
|
54
|
+
</div>
|
55
|
+
</py:with>
|
56
|
+
</fieldset>
|
57
|
+
</py:for>
|
58
|
+
<div class="buttons">
|
59
|
+
<input type="submit" name="cancel" value="Cancel" />
|
60
|
+
<input type="submit" name="save" value="Save" />
|
61
|
+
</div>
|
62
|
+
</form>
|
63
|
+
|
64
|
+
<py:otherwise>
|
65
|
+
<form class="addnew" method="post">
|
66
|
+
<fieldset>
|
67
|
+
<legend>Add Client Event:</legend>
|
68
|
+
<div class="field">
|
69
|
+
<label>Name:<br /><input type="text" name="name" /></label>
|
70
|
+
</div>
|
71
|
+
<div class="field">
|
72
|
+
<label>Summary:<br />
|
73
|
+
<select name="summary">
|
74
|
+
<option py:for="summary in summaries">
|
75
|
+
$summary
|
76
|
+
</option>
|
77
|
+
</select>
|
78
|
+
</label>
|
79
|
+
</div>
|
80
|
+
<div class="field">
|
81
|
+
<label>Action:<br />
|
82
|
+
<select name="action">
|
83
|
+
<option py:for="action in actions">
|
84
|
+
$action
|
85
|
+
</option>
|
86
|
+
</select>
|
87
|
+
</label>
|
88
|
+
</div>
|
89
|
+
<div class="buttons">
|
90
|
+
<input type="submit" name="add" value="Add"/>
|
91
|
+
</div>
|
92
|
+
</fieldset>
|
93
|
+
</form>
|
94
|
+
|
95
|
+
<py:choose>
|
96
|
+
<form py:when="events" method="POST">
|
97
|
+
<table class="listing">
|
98
|
+
<thead>
|
99
|
+
<tr><th class="sel"> </th>
|
100
|
+
<th>Name</th><th>Summary</th><th>Action</th>
|
101
|
+
</tr>
|
102
|
+
</thead>
|
103
|
+
<tbody>
|
104
|
+
<tr py:for="event in events">
|
105
|
+
<td class="sel"><input type="checkbox" name="sel" value="$event.name" /></td>
|
106
|
+
<td class="name">
|
107
|
+
<a href="${panel_href(event.name)}">$event.name</a>
|
108
|
+
</td>
|
109
|
+
<td class="summary">$event.summary</td>
|
110
|
+
<td class="action">$event.action</td>
|
111
|
+
</tr>
|
112
|
+
</tbody>
|
113
|
+
</table>
|
114
|
+
<div class="buttons">
|
115
|
+
<input type="submit" name="remove" value="Remove selected items" />
|
116
|
+
<input type="submit" name="apply" value="Apply changes" />
|
117
|
+
</div>
|
118
|
+
</form>
|
119
|
+
</py:choose>
|
120
|
+
</py:otherwise>
|
121
|
+
</py:choose>
|
122
|
+
</body>
|
123
|
+
|
124
|
+
</html>
|