keithsalisbury-subtrac 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (139) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +7 -0
  3. data/Rakefile +56 -0
  4. data/VERSION.yml +4 -0
  5. data/bin/subtrac +40 -0
  6. data/lib/subtrac.rb +245 -0
  7. data/lib/subtrac/common/clients/index.wsgi +89 -0
  8. data/lib/subtrac/common/favicon.ico +0 -0
  9. data/lib/subtrac/common/images/trac/banner_bg.jpg +0 -0
  10. data/lib/subtrac/common/images/trac/bar_bg.gif +0 -0
  11. data/lib/subtrac/common/images/trac/footer_back.png +0 -0
  12. data/lib/subtrac/common/images/trac/main_bg.gif +0 -0
  13. data/lib/subtrac/common/images/trac/saint_logo_small.png +0 -0
  14. data/lib/subtrac/common/static/404.html +14 -0
  15. data/lib/subtrac/common/styles/trac.css +222 -0
  16. data/lib/subtrac/common/trac.ini +178 -0
  17. data/lib/subtrac/config/config.yml +54 -0
  18. data/lib/subtrac/passwords +1 -0
  19. data/lib/subtrac/shared/trac.ini +178 -0
  20. data/lib/subtrac/templates/location.erb +16 -0
  21. data/lib/subtrac/templates/projects/blank/svn/branches/README +0 -0
  22. data/lib/subtrac/templates/projects/blank/svn/tags/README +0 -0
  23. data/lib/subtrac/templates/projects/blank/svn/trunk/README +0 -0
  24. data/lib/subtrac/templates/projects/blank/trac/wiki/WikiStart +57 -0
  25. data/lib/subtrac/templates/projects/new/svn/trunk/trac/wiki/WikiStart +46 -0
  26. data/lib/subtrac/templates/projects/new/trac/wiki/WikiStart +23 -0
  27. data/lib/subtrac/templates/projects/trac_theme/svn/trunk/index/index.html +22 -0
  28. data/lib/subtrac/templates/projects/trac_theme/svn/trunk/templates/layout.html +56 -0
  29. data/lib/subtrac/templates/projects/trac_theme/svn/trunk/templates/site.html +27 -0
  30. data/lib/subtrac/templates/projects/trac_theme/svn/trunk/templates/theme.html +86 -0
  31. data/lib/subtrac/templates/projects/trac_theme/trac/wiki/WikiStart +4 -0
  32. data/lib/subtrac/templates/trac.erb +25 -0
  33. data/lib/subtrac/templates/vhost.erb +35 -0
  34. data/lib/subtrac/trac-plugins/advancedticketworkflowplugin/advancedworkflow/__init__.py +0 -0
  35. data/lib/subtrac/trac-plugins/advancedticketworkflowplugin/advancedworkflow/controller.py +419 -0
  36. data/lib/subtrac/trac-plugins/advancedticketworkflowplugin/setup.cfg +3 -0
  37. data/lib/subtrac/trac-plugins/advancedticketworkflowplugin/setup.py +20 -0
  38. data/lib/subtrac/trac-plugins/clientsplugin/clients/__init__.py +0 -0
  39. data/lib/subtrac/trac-plugins/clientsplugin/clients/action.py +28 -0
  40. data/lib/subtrac/trac-plugins/clientsplugin/clients/action_email.py +168 -0
  41. data/lib/subtrac/trac-plugins/clientsplugin/clients/action_zendesk_forum.py +137 -0
  42. data/lib/subtrac/trac-plugins/clientsplugin/clients/admin.py +91 -0
  43. data/lib/subtrac/trac-plugins/clientsplugin/clients/api.py +199 -0
  44. data/lib/subtrac/trac-plugins/clientsplugin/clients/client.py +105 -0
  45. data/lib/subtrac/trac-plugins/clientsplugin/clients/events.py +287 -0
  46. data/lib/subtrac/trac-plugins/clientsplugin/clients/eventsadmin.py +71 -0
  47. data/lib/subtrac/trac-plugins/clientsplugin/clients/htdocs/clients.css +4 -0
  48. data/lib/subtrac/trac-plugins/clientsplugin/clients/model.py +135 -0
  49. data/lib/subtrac/trac-plugins/clientsplugin/clients/processor.py +70 -0
  50. data/lib/subtrac/trac-plugins/clientsplugin/clients/reportmanager.py +142 -0
  51. data/lib/subtrac/trac-plugins/clientsplugin/clients/reports.py +231 -0
  52. data/lib/subtrac/trac-plugins/clientsplugin/clients/summary.py +27 -0
  53. data/lib/subtrac/trac-plugins/clientsplugin/clients/summary_milestone.py +152 -0
  54. data/lib/subtrac/trac-plugins/clientsplugin/clients/summary_ticketchanges.py +160 -0
  55. data/lib/subtrac/trac-plugins/clientsplugin/clients/templates/admin_client_events.html +124 -0
  56. data/lib/subtrac/trac-plugins/clientsplugin/clients/templates/admin_clients.html +134 -0
  57. data/lib/subtrac/trac-plugins/clientsplugin/cron/changes.xslt +132 -0
  58. data/lib/subtrac/trac-plugins/clientsplugin/cron/run-client-event +97 -0
  59. data/lib/subtrac/trac-plugins/clientsplugin/cron/summary.xslt +161 -0
  60. data/lib/subtrac/trac-plugins/clientsplugin/setup.py +43 -0
  61. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/__init__.py +4 -0
  62. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/burndownchart.py +273 -0
  63. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/hoursinplaceeditor.py +44 -0
  64. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/hoursremaining.py +36 -0
  65. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/htdocs/jquery-1.2.3.min.js +32 -0
  66. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/htdocs/jquery.jeditable.js +409 -0
  67. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/htdocs/jquery.jeditable.mini.js +30 -0
  68. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/templates/edithours.html +53 -0
  69. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/tests/burndownchart.py +181 -0
  70. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/tests/hoursremaining.py +66 -0
  71. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/tests/workloadchart.py +47 -0
  72. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/utils.py +93 -0
  73. data/lib/subtrac/trac-plugins/estimationtoolsplugin/estimationtools/workloadchart.py +86 -0
  74. data/lib/subtrac/trac-plugins/estimationtoolsplugin/setup.py +20 -0
  75. data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/SumRollups.js +23 -0
  76. data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/adw_tracdb.py +128 -0
  77. data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/git-post-receive +40 -0
  78. data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/trac-post-commit.py +285 -0
  79. data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/trac_billing.py +173 -0
  80. data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/utils/__init__.py +0 -0
  81. data/lib/subtrac/trac-plugins/timingandestimationplugin/scripts/utils/mail.py +164 -0
  82. data/lib/subtrac/trac-plugins/timingandestimationplugin/setup.py +69 -0
  83. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/__init__.py +1 -0
  84. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/api.py +292 -0
  85. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/blackmagic.py +172 -0
  86. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/dbhelper.py +178 -0
  87. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/billingplugin.css +25 -0
  88. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/field_disabler.js +6 -0
  89. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/formatDate.js +356 -0
  90. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/js/tip_centerwindow.js +100 -0
  91. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/js/tip_followscroll.js +84 -0
  92. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/js/wz_tooltip.js +1149 -0
  93. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/linkifyer.js +119 -0
  94. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/query.js +73 -0
  95. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/htdocs/ticket.js +165 -0
  96. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/query_webui.py +28 -0
  97. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/reportmanager.py +221 -0
  98. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/reports.py +675 -0
  99. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/reports_filter.py +150 -0
  100. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/statuses.py +25 -0
  101. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/tande_filters.py +131 -0
  102. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/templates/billing.cs +84 -0
  103. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/templates/billing.html +104 -0
  104. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/ticket_daemon.py +194 -0
  105. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/ticket_policy.py +62 -0
  106. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/ticket_webui.py +28 -0
  107. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/usermanual.py +127 -0
  108. data/lib/subtrac/trac-plugins/timingandestimationplugin/timingandestimationplugin/webui.py +129 -0
  109. data/lib/subtrac/trac-plugins/worklogplugin/setup.py +29 -0
  110. data/lib/subtrac/trac-plugins/worklogplugin/worklog/__init__.py +1 -0
  111. data/lib/subtrac/trac-plugins/worklogplugin/worklog/api.py +187 -0
  112. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/jqModal.css +40 -0
  113. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/jqModal.js +67 -0
  114. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/jquery.mousewheel.pack.js +12 -0
  115. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/jquery.timeentry.pack.js +7 -0
  116. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/tracWorklog.js +40 -0
  117. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/ui.datepicker.css +208 -0
  118. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/ui.datepicker.js +1439 -0
  119. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/work.png +0 -0
  120. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/work.xcf +0 -0
  121. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/worklogplugin.css +80 -0
  122. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/workstart.png +0 -0
  123. data/lib/subtrac/trac-plugins/worklogplugin/worklog/htdocs/workstop.png +0 -0
  124. data/lib/subtrac/trac-plugins/worklogplugin/worklog/manager.py +336 -0
  125. data/lib/subtrac/trac-plugins/worklogplugin/worklog/reports.py +598 -0
  126. data/lib/subtrac/trac-plugins/worklogplugin/worklog/templates/worklog.html +45 -0
  127. data/lib/subtrac/trac-plugins/worklogplugin/worklog/templates/worklog_stop.html +70 -0
  128. data/lib/subtrac/trac-plugins/worklogplugin/worklog/templates/worklog_user.html +40 -0
  129. data/lib/subtrac/trac-plugins/worklogplugin/worklog/templates/worklog_webadminui.html +59 -0
  130. data/lib/subtrac/trac-plugins/worklogplugin/worklog/ticket_daemon.py +33 -0
  131. data/lib/subtrac/trac-plugins/worklogplugin/worklog/ticket_filter.py +153 -0
  132. data/lib/subtrac/trac-plugins/worklogplugin/worklog/timeline_hook.py +96 -0
  133. data/lib/subtrac/trac-plugins/worklogplugin/worklog/usermanual.py +29 -0
  134. data/lib/subtrac/trac-plugins/worklogplugin/worklog/util.py +31 -0
  135. data/lib/subtrac/trac-plugins/worklogplugin/worklog/webadminui.py +47 -0
  136. data/lib/subtrac/trac-plugins/worklogplugin/worklog/webui.py +174 -0
  137. data/lib/subtrac/trac-plugins/worklogplugin/worklog/xmlrpc.py +73 -0
  138. data/lib/subtrac/version.rb +4 -0
  139. metadata +191 -0
@@ -0,0 +1,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 != '&nbsp;':
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 '&nbsp;'
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
+
@@ -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,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