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,173 @@
|
|
1
|
+
from datetime import datetime as dt
|
2
|
+
import time
|
3
|
+
from utils import mail
|
4
|
+
import adw_tracdb as db
|
5
|
+
|
6
|
+
#defaultUrl= "https://sekhemt.acceleration.net/ADW/"
|
7
|
+
_defaultUrl= "https://10.10.10.219/projects"
|
8
|
+
_htmlLocation = '/var/BigVisibleCharts/Billing'
|
9
|
+
|
10
|
+
def cond ( boolExpr, trueResult, falseResult ):
|
11
|
+
""" This is the classic ?: operator from languages like C expressed in python (from dive into python)
|
12
|
+
"""
|
13
|
+
return (boolExpr and [trueResult] or [falseResult])[0]
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
def accumulator():
|
18
|
+
var = [0]
|
19
|
+
def fn(*params):
|
20
|
+
for param in params:
|
21
|
+
var[0] = var[0] + param
|
22
|
+
return var[0]
|
23
|
+
return fn
|
24
|
+
|
25
|
+
def progn(*params):
|
26
|
+
return params[-1]
|
27
|
+
|
28
|
+
def prinTrue(s):
|
29
|
+
print s
|
30
|
+
return True
|
31
|
+
|
32
|
+
|
33
|
+
def wn (name, attribs, *children):
|
34
|
+
return "<%s %s>\n%s\n</%s>\n" % \
|
35
|
+
(name, \
|
36
|
+
' '.join(['%s="%s"' % (key, val) for (key, val) in attribs.items()]), \
|
37
|
+
'\n'.join([str(c) for c in children]), \
|
38
|
+
name)
|
39
|
+
|
40
|
+
def ticket_link (number, projUrl):
|
41
|
+
return wn('a' , {'href':'/'.join([projUrl,'ticket' ,str(number)])}, "#"+str(number))
|
42
|
+
|
43
|
+
def milestone_link (name, projUrl):
|
44
|
+
name = str(name)
|
45
|
+
return wn('a' , {'href':'/'.join([projUrl,'milestone' ,name])}, name)
|
46
|
+
|
47
|
+
def make_project_output(project, rs, totalAcc):
|
48
|
+
projAcc = accumulator()
|
49
|
+
projLink = '/'.join([_defaultUrl, project ])
|
50
|
+
|
51
|
+
def make_cell(idx, val):
|
52
|
+
if(idx == rs.columnMap['ticket']):
|
53
|
+
val = ticket_link(val, projLink)
|
54
|
+
elif idx == rs.columnMap['milestone'] and val != ' ':
|
55
|
+
val = milestone_link(val, projLink)
|
56
|
+
elif idx == rs.columnMap['hours']:
|
57
|
+
projAcc(float(val))
|
58
|
+
totalAcc(float(val))
|
59
|
+
return wn('td', {}, val)
|
60
|
+
|
61
|
+
return progn(wn('div', {},
|
62
|
+
wn('h2', {},
|
63
|
+
wn('a',{'href':projLink},project)),
|
64
|
+
wn('table', {"cellspacing":"0", "border":"1", "cellpadding":"3"},
|
65
|
+
wn('tr', {},
|
66
|
+
*[wn('th',{}, name) for name in rs.columnNames]),
|
67
|
+
*[wn('tr', {},
|
68
|
+
*[make_cell(idx, val)
|
69
|
+
for idx in range(0, len(row))
|
70
|
+
for val in [row[idx]]])
|
71
|
+
for row in rs.rows]),
|
72
|
+
wn('span',{}, "Total: "+str(projAcc()))))
|
73
|
+
|
74
|
+
def make_all_projects_output():
|
75
|
+
totalAcc = accumulator()
|
76
|
+
sql = """
|
77
|
+
SELECT
|
78
|
+
CASE WHEN t.milestone IS NOT NULL and t.milestone <> '' THEN t.milestone
|
79
|
+
ELSE ' '
|
80
|
+
END as milestone,
|
81
|
+
t.id as ticket,
|
82
|
+
SUM(newvalue) as hours,
|
83
|
+
t.summary as summary,
|
84
|
+
strftime('%m/%d/%Y %H:%M:%S', MAX(ticket_change.time), 'unixepoch', 'localtime') as [most-recent-update] ,
|
85
|
+
(SELECT CASE WHEN MAX(time) IS NOT NULL THEN strftime('%m/%d/%Y %H:%M:%S', MAX(time), 'unixepoch', 'localtime')
|
86
|
+
ELSE 'No previous bill date'
|
87
|
+
END as time FROM bill_date ) as [previous-bill-date]
|
88
|
+
FROM ticket as t
|
89
|
+
LEFT JOIN ticket_custom as billable on billable.ticket = t.id
|
90
|
+
AND billable.name = 'billable'
|
91
|
+
JOIN ticket_change on t.id = ticket_change.ticket
|
92
|
+
AND (
|
93
|
+
ticket_change.time >
|
94
|
+
(SELECT CASE WHEN MAX(time) IS NOT NULL THEN MAX(time)
|
95
|
+
ELSE 0
|
96
|
+
END as time FROM bill_date )
|
97
|
+
)
|
98
|
+
WHERE ticket_change.field = 'hours'
|
99
|
+
AND billable.value=1
|
100
|
+
GROUP BY t.milestone, t.id
|
101
|
+
"""
|
102
|
+
|
103
|
+
billingInfo = db.collectResultsFromAllTracs(sql);
|
104
|
+
projects_output = '\n'.join([make_project_output(project, rs, totalAcc)
|
105
|
+
for (project, rs) in billingInfo
|
106
|
+
if rs.rows ])
|
107
|
+
return wn('html', {},
|
108
|
+
wn('head', {}),
|
109
|
+
wn('body', {},
|
110
|
+
projects_output,
|
111
|
+
wn('span',{},"Total hours billed: "+str(totalAcc()))))
|
112
|
+
|
113
|
+
|
114
|
+
def save_output_to_file(output, when=0):
|
115
|
+
if not when:
|
116
|
+
when = dt.now()
|
117
|
+
fname = '_'.join(["billing", str(when.year),
|
118
|
+
str(when.month), str(when.day),
|
119
|
+
str(when.hour), str(when.minute), ".html"])
|
120
|
+
p = "/".join([_htmlLocation, fname])
|
121
|
+
print "----"
|
122
|
+
print "Writing out billing information to '%s'" % p
|
123
|
+
print "----"
|
124
|
+
f = open(p, "w")
|
125
|
+
f.write(output)
|
126
|
+
f.close();
|
127
|
+
|
128
|
+
|
129
|
+
|
130
|
+
def run_billing(emails="ryan@acceleration.net", when=0):
|
131
|
+
if not when:
|
132
|
+
when = dt.now()
|
133
|
+
date = '/'.join([str(when.month), str(when.day), str(when.year)])
|
134
|
+
|
135
|
+
print "Collecting output..."
|
136
|
+
output = make_all_projects_output()
|
137
|
+
save_output_to_file(output, when)
|
138
|
+
print "Emailing results to %s" % emails
|
139
|
+
if emails:
|
140
|
+
mail.mail(emails, 'Trac Billing - %s ' % date, output, html=True, fromEmail='trac-tickets@acceleration.net')
|
141
|
+
return output
|
142
|
+
|
143
|
+
|
144
|
+
|
145
|
+
def add_bill_date(project, username="Timing and Estimation Plugin", when=0):
|
146
|
+
now = time.time()
|
147
|
+
if not when:
|
148
|
+
when = now
|
149
|
+
when = int(when)
|
150
|
+
now = int(now)
|
151
|
+
sql = """
|
152
|
+
INSERT INTO bill_date (time, set_when, str_value)
|
153
|
+
VALUES (?, ?, strftime('%m/%d/%Y %H:%M:%S',?, 'unixepoch', 'localtime'))
|
154
|
+
"""
|
155
|
+
db.executeNonQuery(project, sql, when, now, when)
|
156
|
+
|
157
|
+
def mark_billing_date_in_all_projects(when=0 ):
|
158
|
+
print "Marking the bill date on all projects."
|
159
|
+
if not when:
|
160
|
+
when = time.time()
|
161
|
+
for project in db.projects:
|
162
|
+
try:
|
163
|
+
add_bill_date(project, "Timing and Estimation Plugin", when);
|
164
|
+
print "%s Succeeded." % project
|
165
|
+
except Exception, e:
|
166
|
+
print "* %s failed: %s" % (project , e.args)
|
167
|
+
print "Done marking bill dates"
|
168
|
+
|
169
|
+
|
170
|
+
|
171
|
+
|
172
|
+
|
173
|
+
|
File without changes
|
@@ -0,0 +1,164 @@
|
|
1
|
+
"""
|
2
|
+
Helper library to ease emailing.
|
3
|
+
"""
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
import smtplib
|
8
|
+
import email
|
9
|
+
from email import Encoders
|
10
|
+
from email.Utils import formatdate
|
11
|
+
from email.Message import Message
|
12
|
+
from email.MIMEAudio import MIMEAudio
|
13
|
+
from email.MIMEBase import MIMEBase
|
14
|
+
from email.MIMEMultipart import MIMEMultipart
|
15
|
+
from email.MIMEImage import MIMEImage
|
16
|
+
from email.MIMEText import MIMEText
|
17
|
+
import mimetypes
|
18
|
+
|
19
|
+
FROMEMAIL = 'ryan@acceleration.net'
|
20
|
+
SERVER = 'mail.acceleration.net'
|
21
|
+
|
22
|
+
def rawEmail(toList, msg, server=SERVER, fromEmail=FROMEMAIL):
|
23
|
+
|
24
|
+
s = smtplib.SMTP(server)
|
25
|
+
smtpresult = s.sendmail(fromEmail, toList, msg)
|
26
|
+
|
27
|
+
if smtpresult:
|
28
|
+
errstr = ""
|
29
|
+
for recip in smtpresult.keys():
|
30
|
+
errstr = """Could not delivery mail to: %s
|
31
|
+
|
32
|
+
Server said: %s
|
33
|
+
%s
|
34
|
+
|
35
|
+
%s""" % (recip, smtpresult[recip][0], smtpresult[recip][1], errstr)
|
36
|
+
raise smtplib.SMTPException, errstr
|
37
|
+
|
38
|
+
|
39
|
+
def processTo(to):
|
40
|
+
"""
|
41
|
+
helper function that processes a string or list of addresses.
|
42
|
+
|
43
|
+
returns (list of addresses, comma-delimited string of addresses)
|
44
|
+
|
45
|
+
The first is used for rawEmail, and the second is used when creating
|
46
|
+
headers.
|
47
|
+
"""
|
48
|
+
toList = []
|
49
|
+
if type(to) is list:
|
50
|
+
toList = to[:]
|
51
|
+
to = ", ".join(to)
|
52
|
+
else:
|
53
|
+
toList = [to]
|
54
|
+
|
55
|
+
return (toList, to)
|
56
|
+
|
57
|
+
def mail(to, subject, message, html=False, fromEmail=FROMEMAIL, server=SERVER):
|
58
|
+
"""
|
59
|
+
Simplifies the emailing process, sending plaintext emails.
|
60
|
+
|
61
|
+
to: accepts a list of emails or a single email.
|
62
|
+
|
63
|
+
returns nothing, or throws an smtplib.SMTPException if there is a problem.
|
64
|
+
"""
|
65
|
+
toList, to = processTo(to)
|
66
|
+
|
67
|
+
htmlHeader = '\n'
|
68
|
+
|
69
|
+
if html:
|
70
|
+
htmlHeader = 'Content-Type: text/html; charset=ISO-8859-1\n\n'
|
71
|
+
|
72
|
+
msg = '''To: %s
|
73
|
+
From: %s
|
74
|
+
Subject: %s
|
75
|
+
Date: %s
|
76
|
+
%s
|
77
|
+
%s
|
78
|
+
''' % (to, fromEmail, subject, formatdate(), htmlHeader, message)
|
79
|
+
|
80
|
+
rawEmail(toList, msg, fromEmail=fromEmail, server=server)
|
81
|
+
|
82
|
+
|
83
|
+
def sms_me(subject, message):
|
84
|
+
" sends an email to ryan's phone "
|
85
|
+
mail('sms-ryan@acceleration.net', subject, message)
|
86
|
+
|
87
|
+
def emailHtml(to, subject, message):
|
88
|
+
toList, to = processTo(to)
|
89
|
+
msg = MIMEMultipart()
|
90
|
+
msg['Subject'] = subject
|
91
|
+
msg['To'] = to
|
92
|
+
msg['From'] = fromEmail
|
93
|
+
msg['Date'] = formatdate()
|
94
|
+
msg.preamble = 'You are not using a MIME-aware reader\n'
|
95
|
+
msg.epilogue = ''
|
96
|
+
|
97
|
+
#add the main message
|
98
|
+
msg.attach(MIMEText(message))
|
99
|
+
|
100
|
+
rawEmail(toList, msg.as_string())
|
101
|
+
|
102
|
+
def emailFile(to, subject, message, filePath, mimeType=None, fromEmail=FROMEMAIL, server=SERVER):
|
103
|
+
"""
|
104
|
+
sends an email attachment to the given address or list of addesses.
|
105
|
+
|
106
|
+
if the mimeType is not specified, it uses mimetypes.guess_type to determine it.
|
107
|
+
"""
|
108
|
+
toList, to = processTo(to)
|
109
|
+
|
110
|
+
msg = MIMEMultipart()
|
111
|
+
msg['Subject'] = subject
|
112
|
+
msg['To'] = to
|
113
|
+
msg['From'] = fromEmail
|
114
|
+
msg['Date'] = formatdate()
|
115
|
+
msg.preamble = 'You are not using a MIME-aware reader\n'
|
116
|
+
msg.epilogue = ''
|
117
|
+
|
118
|
+
#add the main message
|
119
|
+
msg.attach(MIMEText(message))
|
120
|
+
|
121
|
+
if type(filePath) is list:
|
122
|
+
for f in filePath:
|
123
|
+
addFile(f, msg, mimeType)
|
124
|
+
else:
|
125
|
+
addFile(filePath, msg, mimeType)
|
126
|
+
|
127
|
+
rawEmail(toList, msg.as_string(), server=server, fromEmail=fromEmail)
|
128
|
+
|
129
|
+
def addFile(filePath, message, mimeType=None):
|
130
|
+
if mimeType is None:
|
131
|
+
mimeType = mimetypes.guess_type(filePath)[0]
|
132
|
+
|
133
|
+
maintype, subtype = mimeType.split('/', 1)
|
134
|
+
|
135
|
+
def processFactory(mimeRunner, openMethod):
|
136
|
+
def proc(path, subtype):
|
137
|
+
fp = open(path, openMethod)
|
138
|
+
fileMsg = mimeRunner(fp.read(), _subtype = subtype)
|
139
|
+
fp.close()
|
140
|
+
return fileMsg
|
141
|
+
|
142
|
+
return proc
|
143
|
+
|
144
|
+
messageMaker = {'text':processFactory(MIMEText, 'r'),
|
145
|
+
'image':processFactory(MIMEImage, 'rb'),
|
146
|
+
'audio':processFactory(MIMEAudio, 'rb')}
|
147
|
+
|
148
|
+
|
149
|
+
|
150
|
+
fMsg = None
|
151
|
+
if messageMaker.has_key(maintype):
|
152
|
+
fMsg = messageMaker[maintype](filePath, subtype)
|
153
|
+
else:
|
154
|
+
fp = open(filePath, 'rb')
|
155
|
+
fMsg = MIMEBase(maintype, subtype)
|
156
|
+
fMsg.set_payload(fp.read())
|
157
|
+
fp.close()
|
158
|
+
# Encode the payload using Base64
|
159
|
+
Encoders.encode_base64(fMsg)
|
160
|
+
|
161
|
+
if fMsg is not None:
|
162
|
+
fMsg.add_header('Content-Disposition', 'attachment', filename=filePath)
|
163
|
+
message.attach(fMsg)
|
164
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
from setuptools import setup
|
4
|
+
|
5
|
+
PACKAGE = 'timingandestimationplugin'
|
6
|
+
|
7
|
+
setup(name=PACKAGE,
|
8
|
+
description='Plugin to make Trac support time estimation and tracking with permissions',
|
9
|
+
keywords='trac plugin estimation timetracking permissions',
|
10
|
+
version='0.7.8',
|
11
|
+
url='http://www.trac-hacks.org/wiki/TimingAndEstimationPlugin',
|
12
|
+
license='http://www.opensource.org/licenses/mit-license.php',
|
13
|
+
author='Russ Tyndall at Acceleration.net',
|
14
|
+
author_email='russ@acceleration.net',
|
15
|
+
long_description="""
|
16
|
+
This Trac 0.11 plugin provides support for Time estimation and tracking,
|
17
|
+
and permissions to view and set those fields
|
18
|
+
|
19
|
+
See http://trac-hacks.org/wiki/TimingAndEstimationPlugin for details.
|
20
|
+
""",
|
21
|
+
packages=[PACKAGE],
|
22
|
+
package_data={PACKAGE : ['templates/*.html', 'htdocs/js/*', 'htdocs/*.css', 'htdocs/*.js']},
|
23
|
+
entry_points={'trac.plugins': '%s = %s' % (PACKAGE, PACKAGE)})
|
24
|
+
|
25
|
+
|
26
|
+
#### FINANCIAL CONTRIBUTERS ####
|
27
|
+
#
|
28
|
+
# Obsidian Software: http://www.obsidiansoft.com/
|
29
|
+
# Enterprise Solutions for Functional Processor
|
30
|
+
# Design Verification
|
31
|
+
#
|
32
|
+
################################
|
33
|
+
|
34
|
+
#### AUTHORS ####
|
35
|
+
## Primary Author:
|
36
|
+
## Russell Tyndall
|
37
|
+
## Acceleration.net
|
38
|
+
## russ@acceleration.net
|
39
|
+
## trac-hacks user: bobbysmith007
|
40
|
+
|
41
|
+
##
|
42
|
+
|
43
|
+
## Alessio Massaro
|
44
|
+
## trac-hacks user: masariello
|
45
|
+
## Helped Get Reports working in postgres
|
46
|
+
## and started moving toward generic work
|
47
|
+
## rather than hours
|
48
|
+
|
49
|
+
## kkurzweil@lulu.com
|
50
|
+
## helped postegresql db backend compatiblity
|
51
|
+
|
52
|
+
## jonas
|
53
|
+
## made it so that base_url was unnecessary
|
54
|
+
|
55
|
+
## Colin Guthrie
|
56
|
+
## trac-hacks user: coling
|
57
|
+
## Refactored the custom reports code to make it
|
58
|
+
## easy for other plugins to provide reports to
|
59
|
+
## compliment those provided by default
|
60
|
+
## Added Javascript that improves Ticket UI
|
61
|
+
|
62
|
+
## Dave Abrahams <dave@boost-consulting.com>
|
63
|
+
##
|
64
|
+
## Genshi filters to remove T&E reports from the
|
65
|
+
## standard reports page, where they display errors
|
66
|
+
|
67
|
+
## Greg Troxel
|
68
|
+
##
|
69
|
+
## Updated the post commit hooks to be inline with upstream trac
|
@@ -0,0 +1 @@
|
|
1
|
+
from api import *
|
@@ -0,0 +1,292 @@
|
|
1
|
+
import re
|
2
|
+
import dbhelper
|
3
|
+
import time
|
4
|
+
from tande_filters import *
|
5
|
+
from reports_filter import *
|
6
|
+
from blackmagic import *
|
7
|
+
from ticket_daemon import *
|
8
|
+
from ticket_webui import *
|
9
|
+
from usermanual import *
|
10
|
+
from ticket_policy import *
|
11
|
+
from trac.log import logger_factory
|
12
|
+
from trac.ticket import ITicketChangeListener, Ticket
|
13
|
+
from trac.core import *
|
14
|
+
from trac.env import IEnvironmentSetupParticipant
|
15
|
+
from trac.perm import IPermissionRequestor, PermissionSystem
|
16
|
+
from webui import *
|
17
|
+
from query_webui import *
|
18
|
+
from reportmanager import CustomReportManager
|
19
|
+
from statuses import *
|
20
|
+
from reports import all_reports
|
21
|
+
from sets import Set
|
22
|
+
|
23
|
+
## report columns
|
24
|
+
## id|author|title|query|description
|
25
|
+
|
26
|
+
class TimeTrackingSetupParticipant(Component):
|
27
|
+
""" This is the config that must be there for this plugin to work:
|
28
|
+
|
29
|
+
[ticket-custom]
|
30
|
+
totalhours = text
|
31
|
+
totalhours.value = 0
|
32
|
+
totalhours.label = Total Hours
|
33
|
+
|
34
|
+
billable = checkbox
|
35
|
+
billable.value = 1
|
36
|
+
billable.label = Is this billable?
|
37
|
+
|
38
|
+
hours = text
|
39
|
+
hours.value = 0
|
40
|
+
hours.label = Hours to Add
|
41
|
+
|
42
|
+
estimatedhours = text
|
43
|
+
estimatedhours.value = 0
|
44
|
+
estimatedhours.label = Estimated Hours?
|
45
|
+
|
46
|
+
internal = checkbox
|
47
|
+
internal.value = 0
|
48
|
+
internal.label = Internal?
|
49
|
+
|
50
|
+
"""
|
51
|
+
implements(IEnvironmentSetupParticipant)
|
52
|
+
db_version_key = None
|
53
|
+
db_version = None
|
54
|
+
db_installed_version = None
|
55
|
+
|
56
|
+
"""Extension point interface for components that need to participate in the
|
57
|
+
creation and upgrading of Trac environments, for example to create
|
58
|
+
additional database tables."""
|
59
|
+
def __init__(self):
|
60
|
+
# Setup logging
|
61
|
+
self.statuses_key = 'T&E-statuses'
|
62
|
+
self.db_version_key = 'TimingAndEstimationPlugin_Db_Version'
|
63
|
+
self.db_version = 8
|
64
|
+
# Initialise database schema version tracking.
|
65
|
+
self.db_installed_version = dbhelper.get_system_value(self, \
|
66
|
+
self.db_version_key) or 0
|
67
|
+
|
68
|
+
def environment_created(self):
|
69
|
+
"""Called when a new Trac environment is created."""
|
70
|
+
if self.environment_needs_upgrade(None):
|
71
|
+
self.upgrade_environment(None)
|
72
|
+
|
73
|
+
|
74
|
+
def system_needs_upgrade(self):
|
75
|
+
return self.db_installed_version < self.db_version
|
76
|
+
|
77
|
+
def do_db_upgrade(self):
|
78
|
+
if self.db_installed_version < 1:
|
79
|
+
print "Creating bill_date table"
|
80
|
+
sql = """
|
81
|
+
CREATE TABLE bill_date (
|
82
|
+
time integer,
|
83
|
+
set_when integer,
|
84
|
+
str_value text
|
85
|
+
);
|
86
|
+
"""
|
87
|
+
dbhelper.execute_non_query(self, sql)
|
88
|
+
|
89
|
+
|
90
|
+
if self.db_installed_version < 5:
|
91
|
+
if dbhelper.db_table_exists(self, 'report_version'):
|
92
|
+
print "Dropping report_version table"
|
93
|
+
sql = "DELETE FROM report " \
|
94
|
+
"WHERE author=%s AND id IN (SELECT report FROM report_version)"
|
95
|
+
dbhelper.execute_non_query(self, sql, 'Timing and Estimation Plugin')
|
96
|
+
|
97
|
+
sql = "DROP TABLE report_version"
|
98
|
+
dbhelper.execute_non_query(self, sql)
|
99
|
+
|
100
|
+
#version 6 upgraded reports
|
101
|
+
|
102
|
+
|
103
|
+
if self.db_installed_version < 7:
|
104
|
+
field_settings = "field settings"
|
105
|
+
self.config.set( field_settings, "fields", "billable, totalhours, hours, estimatedhours, internal" )
|
106
|
+
self.config.set( field_settings, "billable.permission", "TIME_VIEW:hide, TIME_RECORD:disable" )
|
107
|
+
self.config.set( field_settings, "hours.permission", "TIME_VIEW:remove, TIME_RECORD:disable" )
|
108
|
+
self.config.set( field_settings, "estimatedhours.permission", "TIME_RECORD:disable" )
|
109
|
+
self.config.set( field_settings, "internal.permission", "TIME_RECORD:hide")
|
110
|
+
|
111
|
+
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
112
|
+
# This statement block always goes at the end this method
|
113
|
+
dbhelper.set_system_value(self, self.db_version_key, self.db_version)
|
114
|
+
self.db_installed_version = self.db_version
|
115
|
+
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
116
|
+
|
117
|
+
|
118
|
+
|
119
|
+
def reports_need_upgrade(self):
|
120
|
+
mgr = CustomReportManager(self.env, self.log)
|
121
|
+
db_reports = mgr.get_version_hash_by_group(CustomReportManager.TimingAndEstimationKey)
|
122
|
+
py_reports = {}
|
123
|
+
for report_group in all_reports:
|
124
|
+
for report in report_group['reports']:
|
125
|
+
py_reports[report['uuid']]= report['version']
|
126
|
+
|
127
|
+
diff = [(uuid, version) for (uuid, version) in py_reports.items()
|
128
|
+
if not db_reports.has_key(uuid) or int(db_reports[uuid]) < int(version)]
|
129
|
+
|
130
|
+
if len(diff) > 0:
|
131
|
+
self.log.debug ("T&E needs upgrades for the following reports: %s" %
|
132
|
+
(diff, ))
|
133
|
+
return len(diff) > 0
|
134
|
+
|
135
|
+
def do_reports_upgrade(self, force=False):
|
136
|
+
self.log.debug( "Beginning Reports Upgrade");
|
137
|
+
mgr = CustomReportManager(self.env, self.log)
|
138
|
+
statuses = get_statuses(self)
|
139
|
+
stat_vars = status_variables(statuses)
|
140
|
+
|
141
|
+
for report_group in all_reports:
|
142
|
+
rlist = report_group["reports"]
|
143
|
+
group_title = report_group["title"]
|
144
|
+
for report in rlist:
|
145
|
+
title = report["title"]
|
146
|
+
new_version = report["version"]
|
147
|
+
|
148
|
+
sql = report["sql"].replace('#STATUSES#', stat_vars)
|
149
|
+
mgr.add_report(report["title"], "Timing and Estimation Plugin", \
|
150
|
+
"Reports Must Be Accessed From the Management Screen",
|
151
|
+
sql, report["uuid"], report["version"],
|
152
|
+
CustomReportManager.TimingAndEstimationKey,
|
153
|
+
group_title, force)
|
154
|
+
|
155
|
+
def ticket_fields_need_upgrade(self):
|
156
|
+
ticket_custom = "ticket-custom"
|
157
|
+
return not ( self.config.get( ticket_custom, "totalhours" ) and \
|
158
|
+
self.config.get( ticket_custom, "hours" ) and \
|
159
|
+
self.config.get( ticket_custom, "totalhours.order") and \
|
160
|
+
self.config.get( ticket_custom, "hours.order") and \
|
161
|
+
self.config.get( ticket_custom, "estimatedhours.order") and \
|
162
|
+
self.config.get( ticket_custom, "estimatedhours") and \
|
163
|
+
self.config.get( ticket_custom, "internal") and \
|
164
|
+
"InternalTicketsPolicy" in self.config.getlist("trac", "permission_policies"))
|
165
|
+
|
166
|
+
def do_ticket_field_upgrade(self):
|
167
|
+
ticket_custom = "ticket-custom"
|
168
|
+
|
169
|
+
if not self.config.get(ticket_custom,"totalhours"):
|
170
|
+
self.config.set(ticket_custom,"totalhours", "text")
|
171
|
+
self.config.set(ticket_custom,"totalhours.order", "4")
|
172
|
+
self.config.set(ticket_custom,"totalhours.value", "0")
|
173
|
+
self.config.set(ticket_custom,"totalhours.label", "Total Hours")
|
174
|
+
|
175
|
+
|
176
|
+
if not self.config.get(ticket_custom,"billable"):
|
177
|
+
self.config.set(ticket_custom,"billable", "checkbox")
|
178
|
+
self.config.set(ticket_custom,"billable.value", "1")
|
179
|
+
self.config.set(ticket_custom,"billable.order", "3")
|
180
|
+
self.config.set(ticket_custom,"billable.label", "Billable?")
|
181
|
+
|
182
|
+
if not self.config.get(ticket_custom,"hours"):
|
183
|
+
self.config.set(ticket_custom,"hours", "text")
|
184
|
+
self.config.set(ticket_custom,"hours.value", "0")
|
185
|
+
self.config.set(ticket_custom,"hours.order", "2")
|
186
|
+
self.config.set(ticket_custom,"hours.label", "Add Hours to Ticket")
|
187
|
+
|
188
|
+
if not self.config.get(ticket_custom,"estimatedhours"):
|
189
|
+
self.config.set(ticket_custom,"estimatedhours", "text")
|
190
|
+
self.config.set(ticket_custom,"estimatedhours.value", "0")
|
191
|
+
self.config.set(ticket_custom,"estimatedhours.order", "1")
|
192
|
+
self.config.set(ticket_custom,"estimatedhours.label", "Estimated Number of Hours")
|
193
|
+
|
194
|
+
if not self.config.get( ticket_custom, "internal"):
|
195
|
+
self.config.set(ticket_custom, "internal", "checkbox")
|
196
|
+
self.config.set(ticket_custom, "internal.value", "0")
|
197
|
+
self.config.set(ticket_custom, "internal.label", "Internal?")
|
198
|
+
self.config.set(ticket_custom,"internal.order", "5")
|
199
|
+
|
200
|
+
if "InternalTicketsPolicy" not in self.config.getlist("trac", "permission_policies"):
|
201
|
+
perms = ["InternalTicketsPolicy"]
|
202
|
+
other_policies = self.config.getlist("trac", "permission_policies")
|
203
|
+
if "DefaultPermissionPolicy" not in other_policies:
|
204
|
+
perms.append("DefaultPermissionPolicy")
|
205
|
+
perms.extend( other_policies )
|
206
|
+
self.config.set("trac", "permission_policies", ', '.join(perms))
|
207
|
+
|
208
|
+
self.config.save();
|
209
|
+
|
210
|
+
def needs_user_man(self):
|
211
|
+
maxversion = dbhelper.get_scalar(self, "SELECT MAX(version) FROM wiki WHERE name like %s", 0,
|
212
|
+
user_manual_wiki_title)
|
213
|
+
if (not maxversion) or maxversion < user_manual_version:
|
214
|
+
return True
|
215
|
+
return False
|
216
|
+
|
217
|
+
def do_user_man_update(self):
|
218
|
+
|
219
|
+
when = int(time.time())
|
220
|
+
sql = """
|
221
|
+
INSERT INTO wiki (name,version,time,author,ipnr,text,comment,readonly)
|
222
|
+
VALUES ( %s, %s, %s, 'Timing and Estimation Plugin', '127.0.0.1', %s,'',0)
|
223
|
+
"""
|
224
|
+
dbhelper.execute_non_query(self, sql,
|
225
|
+
user_manual_wiki_title,
|
226
|
+
user_manual_version,
|
227
|
+
when,
|
228
|
+
user_manual_content)
|
229
|
+
|
230
|
+
|
231
|
+
def environment_needs_upgrade(self, db):
|
232
|
+
"""Called when Trac checks whether the environment needs to be upgraded.
|
233
|
+
|
234
|
+
Should return `True` if this participant needs an upgrade to be
|
235
|
+
performed, `False` otherwise.
|
236
|
+
|
237
|
+
"""
|
238
|
+
self.log.debug("NEEDS UP?: sys:%s, rep:%s, stats:%s, fields:%s, man:%s" % \
|
239
|
+
((self.system_needs_upgrade()),
|
240
|
+
(self.reports_need_upgrade()),
|
241
|
+
(self.have_statuses_changed()),
|
242
|
+
(self.ticket_fields_need_upgrade()),
|
243
|
+
(self.needs_user_man())))
|
244
|
+
return (self.system_needs_upgrade()) or \
|
245
|
+
(self.reports_need_upgrade()) or \
|
246
|
+
(self.have_statuses_changed()) or \
|
247
|
+
(self.ticket_fields_need_upgrade()) or \
|
248
|
+
(self.needs_user_man())
|
249
|
+
|
250
|
+
def upgrade_environment(self, db):
|
251
|
+
"""Actually perform an environment upgrade.
|
252
|
+
|
253
|
+
Implementations of this method should not commit any database
|
254
|
+
transactions. This is done implicitly after all participants have
|
255
|
+
performed the upgrades they need without an error being raised.
|
256
|
+
"""
|
257
|
+
def p(s):
|
258
|
+
print s
|
259
|
+
return True
|
260
|
+
print "Timing and Estimation needs an upgrade"
|
261
|
+
p("Upgrading Database")
|
262
|
+
self.do_db_upgrade()
|
263
|
+
p("Upgrading reports")
|
264
|
+
self.do_reports_upgrade(force=self.have_statuses_changed())
|
265
|
+
|
266
|
+
#make sure we upgrade the statuses string so that we dont need to always rebuild the
|
267
|
+
# reports
|
268
|
+
stats = get_statuses(self)
|
269
|
+
val = ','.join(list(stats))
|
270
|
+
dbhelper.set_system_value(self, self.statuses_key, val)
|
271
|
+
|
272
|
+
if self.ticket_fields_need_upgrade():
|
273
|
+
p("Upgrading fields")
|
274
|
+
self.do_ticket_field_upgrade()
|
275
|
+
if self.needs_user_man():
|
276
|
+
p("Upgrading usermanual")
|
277
|
+
self.do_user_man_update()
|
278
|
+
print "Done Upgrading"
|
279
|
+
|
280
|
+
def have_statuses_changed(self):
|
281
|
+
"""get the statuses from the last time we saved them,
|
282
|
+
compare them to the ones we have now (ignoring '' and None),
|
283
|
+
if we have different ones, throw return true
|
284
|
+
"""
|
285
|
+
s = dbhelper.get_system_value(self, self.statuses_key)
|
286
|
+
if not s:
|
287
|
+
return True
|
288
|
+
sys_stats = get_statuses(self)
|
289
|
+
s = s.split(',')
|
290
|
+
sys_stats.symmetric_difference_update(s)
|
291
|
+
sys_stats.difference_update(['', None])
|
292
|
+
return len(sys_stats) > 0
|