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,194 @@
1
+ from trac.ticket import ITicketChangeListener, Ticket, ITicketManipulator
2
+ from trac.perm import PermissionCache
3
+ from trac.core import *
4
+ import datetime
5
+
6
+ def identity(x):
7
+ return x;
8
+
9
+ try:
10
+ import trac.util.datefmt
11
+ to_timestamp = trac.util.datefmt.to_timestamp
12
+ except Exception:
13
+ to_timestamp = identity
14
+
15
+
16
+ def save_custom_field_value( db, ticket_id, field, value ):
17
+ cursor = db.cursor();
18
+ cursor.execute("SELECT * FROM ticket_custom "
19
+ "WHERE ticket=%s and name=%s", (ticket_id, field))
20
+ if cursor.fetchone():
21
+ cursor.execute("UPDATE ticket_custom SET value=%s "
22
+ "WHERE ticket=%s AND name=%s",
23
+ (value, ticket_id, field))
24
+ else:
25
+ cursor.execute("INSERT INTO ticket_custom (ticket,name, "
26
+ "value) VALUES(%s,%s,%s)",
27
+ (ticket_id, field, value))
28
+ db.commit()
29
+
30
+ DONTUPDATE = "DONTUPDATE"
31
+
32
+ def save_ticket_change( db, ticket_id, author, change_time, field, oldvalue, newvalue, log, dontinsert=False):
33
+ """tries to save a ticket change,
34
+
35
+ dontinsert means do not add the change if it didnt already exist
36
+ """
37
+ if type(change_time) == datetime.datetime:
38
+ change_time = to_timestamp(change_time)
39
+ cursor = db.cursor();
40
+ sql = """SELECT * FROM ticket_change
41
+ WHERE ticket=%s and author=%s and time=%s and field=%s"""
42
+
43
+ cursor.execute(sql, (ticket_id, author, change_time, field))
44
+ if cursor.fetchone():
45
+ if oldvalue == DONTUPDATE:
46
+ cursor.execute("""UPDATE ticket_change SET newvalue=%s
47
+ WHERE ticket=%s and author=%s and time=%s and field=%s""",
48
+ ( newvalue, ticket_id, author, change_time, field))
49
+
50
+ else:
51
+ cursor.execute("""UPDATE ticket_change SET oldvalue=%s, newvalue=%s
52
+ WHERE ticket=%s and author=%s and time=%s and field=%s""",
53
+ (oldvalue, newvalue, ticket_id, author, change_time, field))
54
+ else:
55
+ if oldvalue == DONTUPDATE:
56
+ oldvalue = '0'
57
+ if not dontinsert:
58
+ cursor.execute("""INSERT INTO ticket_change (ticket,time,author,field, oldvalue, newvalue)
59
+ VALUES(%s, %s, %s, %s, %s, %s)""",
60
+ (ticket_id, change_time, author, field, oldvalue, newvalue))
61
+ db.commit()
62
+
63
+ def delete_ticket_change( comp, ticket_id, author, change_time, field):
64
+ """ removes a ticket change from the database """
65
+ db=comp.env.get_db_cnx()
66
+ cursor = db.cursor();
67
+ sql = """DELETE FROM ticket_change
68
+ WHERE ticket=%s and author=%s and time=%s and field=%s"""
69
+ cursor.execute(sql, (ticket_id, author, change_time, field))
70
+ db.commit()
71
+
72
+
73
+
74
+
75
+ class TimeTrackingTicketObserver(Component):
76
+ implements(ITicketChangeListener)
77
+ def __init__(self):
78
+ pass
79
+
80
+ def watch_hours(self, ticket):
81
+ def readTicketValue(name, tipe, default=0):
82
+ if ticket.values.has_key(name):
83
+ return tipe(ticket.values[name] or default)
84
+ else:
85
+ cursor = self.env.get_db_cnx().cursor()
86
+ cursor.execute("SELECT * FROM ticket_custom where ticket=%s and name=%s" , (ticket.id, name))
87
+ val = cursor.fetchone()
88
+ if val:
89
+ return tipe(val[2] or default)
90
+ return default
91
+
92
+ #some european countries use , as the decimal separator
93
+ convertfloat = lambda x: float(str(x).replace(',','.'))
94
+ hours = readTicketValue("hours", convertfloat)
95
+ totalHours = readTicketValue("totalhours", convertfloat)
96
+
97
+ db = self.env.get_db_cnx()
98
+ ticket_id = ticket.id
99
+ cl = ticket.get_changelog()
100
+
101
+ self.log.debug("found hours: "+str(hours ));
102
+ #self.log.debug("Dir_ticket:"+str(dir(ticket)))
103
+ #self.log.debug("ticket.values:"+str(ticket.values))
104
+ #self.log.debug("changelog:"+str(cl))
105
+
106
+ most_recent_change = None
107
+ if cl:
108
+ most_recent_change = cl[-1];
109
+ change_time = most_recent_change[0]
110
+ author = most_recent_change[1]
111
+ else:
112
+ change_time = ticket.time_created
113
+ author = ticket.values["reporter"]
114
+
115
+ self.log.debug("Checking permissions")
116
+ perm = PermissionCache(self.env, author)
117
+ if not perm or not perm.has_permission("TIME_RECORD"):
118
+ self.log.debug("Skipping recording because no permission to affect time")
119
+ if hours != 0:
120
+
121
+ tup = (ticket_id, author, change_time, "hours")
122
+ self.log.debug("deleting ticket change %s %s %s %s" % tup)
123
+ try:
124
+ delete_ticket_change(self, ticket_id, author, change_time, "hours")
125
+ except Exception, e:
126
+ self.log.debug("FAIL: %s" % e)
127
+ self.log.debug("hours change deleted")
128
+ return
129
+ self.log.debug("passed permissions check")
130
+
131
+ ## SAVE estimated hour
132
+ estimatedhours = readTicketValue("estimatedhours", convertfloat)
133
+ self.log.debug("found Estimated hours:"+str(estimatedhours))
134
+ db = self.env.get_db_cnx()
135
+ save_ticket_change( db, ticket_id, author, change_time, "estimatedhours", DONTUPDATE, str(estimatedhours), self.log, True)
136
+ save_custom_field_value( db, ticket.id, "estimatedhours", str(estimatedhours))
137
+ db.commit();
138
+ #######################
139
+
140
+
141
+ ## If our hours changed
142
+ if not hours == 0:
143
+ newtotal = str(totalHours+hours)
144
+ save_ticket_change( db, ticket_id, author, change_time, "hours", '0.0', str(hours), self.log)
145
+ save_ticket_change( db, ticket_id, author, change_time, "totalhours", str(totalHours), str(newtotal), self.log)
146
+ save_custom_field_value( db, ticket_id, "hours", '0')
147
+ save_custom_field_value( db, ticket_id, "totalhours", str(newtotal) )
148
+ ########################
149
+
150
+ # END of watch_hours
151
+
152
+ def ticket_created(self, ticket):
153
+ """Called when a ticket is created."""
154
+ self.watch_hours(ticket)
155
+
156
+
157
+ def ticket_changed(self, ticket, comment, author, old_values):
158
+ """Called when a ticket is modified.
159
+
160
+ `old_values` is a dictionary containing the previous values of the
161
+ fields that have changed.
162
+ """
163
+ self.watch_hours(ticket)
164
+
165
+ def ticket_deleted(self, ticket):
166
+ """Called when a ticket is deleted."""
167
+
168
+ class TimeTrackingTicketValidator(Component):
169
+ implements(ITicketManipulator)
170
+
171
+ def __init__(self):
172
+ pass
173
+
174
+ def prepare_ticket(req, ticket, fields, actions):
175
+ """not currently called"""
176
+
177
+ def validate_ticket(self, req, ticket):
178
+ """Validate a ticket after it's been populated from user input.
179
+
180
+ Must return a list of `(field, message)` tuples, one for each problem
181
+ detected. `field` can be `None` to indicate an overall problem with the
182
+ ticket. Therefore, a return value of `[]` means everything is OK."""
183
+ errors = []
184
+ #some european countries use , as the decimal separator
185
+ convertfloat = lambda x: float(str(x).replace(',','.'))
186
+ try:
187
+ convertfloat(ticket.values['hours'])
188
+ except ValueError:
189
+ errors.append(('Add Hours to Ticket', 'Value must be a number'))
190
+ try:
191
+ convertfloat(ticket.values['estimatedhours'])
192
+ except ValueError:
193
+ errors.append(('Estimated Number of Hours', 'Value must be a number'))
194
+ return errors
@@ -0,0 +1,62 @@
1
+ from trac.core import *
2
+ from trac.perm import PermissionCache, IPermissionRequestor, IPermissionGroupProvider, IPermissionPolicy, PermissionSystem
3
+ from trac.ticket.model import Ticket
4
+ from trac.config import IntOption, ListOption
5
+ from trac.util.compat import set
6
+
7
+ class InternalTicketsPolicy(Component):
8
+ """Hide internal tickets."""
9
+ implements(IPermissionPolicy)
10
+ group_providers = ExtensionPoint(IPermissionGroupProvider)
11
+
12
+ # IPermissionPolicy(Interface)
13
+ def check_permission(self, action, username, resource, perm):
14
+ self.log.debug("Internal: action:%s, user:%s, resource:%s, perm: %s" %
15
+ ( action, username, resource, perm))
16
+ self.username = username
17
+ # Look up the resource parentage for a ticket.
18
+ while resource:
19
+ if resource.realm == 'ticket':
20
+ break
21
+ resource = resource.parent
22
+ if resource and resource.realm == 'ticket' and resource.id is not None:
23
+ rtn = self.check_ticket_access(perm, resource, username)
24
+ self.log.debug("Internal: RESULTS for %s: %s" % (action,rtn))
25
+ return rtn
26
+ return None
27
+
28
+ # Internal methods
29
+ def _get_groups(self, user):
30
+ # Get initial subjects
31
+ groups = set([user])
32
+ for provider in self.group_providers:
33
+ for group in provider.get_permission_groups(user):
34
+ groups.add(group)
35
+
36
+ perms = PermissionSystem(self.env).get_all_permissions()
37
+ repeat = True
38
+ while repeat:
39
+ repeat = False
40
+ for subject, action in perms:
41
+ if subject in groups and action.islower() and action not in groups:
42
+ groups.add(action)
43
+ repeat = True
44
+
45
+ return groups
46
+
47
+ # Public methods
48
+ def check_ticket_access(self, perm, res, user):
49
+ """Return if this req is permitted access to the given ticket ID."""
50
+ try:
51
+ tkt = Ticket(self.env, res.id)
52
+ except TracError:
53
+ return None # Ticket doesn't exist
54
+ private_tkt = tkt['internal'] == '1'
55
+
56
+ if private_tkt:
57
+ # cant just check or we get in an infinite call loop
58
+ perm = PermissionCache(self.env, self.username, None, perm._cache)
59
+ groups = self._get_groups(user)
60
+ perm_or_group = self.config.get('ticket', 'internalgroup', 'TIME_ADMIN' )
61
+ return perm_or_group in groups or perm.has_permission(perm_or_group)
62
+ return None
@@ -0,0 +1,28 @@
1
+ import re
2
+ from trac.web.api import ITemplateStreamFilter
3
+ from trac.log import logger_factory
4
+ from trac.core import *
5
+ from genshi.builder import tag
6
+ from trac.web import IRequestHandler
7
+ from trac.util import Markup
8
+ from trac.web.href import Href
9
+ from genshi.filters.transform import Transformer
10
+
11
+
12
+ class TicketWebUiAddon(Component):
13
+ implements(ITemplateStreamFilter)
14
+
15
+ def __init__(self):
16
+ pass
17
+
18
+ # ITemplateStreamFilter
19
+ def filter_stream(self, req, method, filename, stream, data):
20
+ self.log.debug("TicketWebUiAddon executing")
21
+ if not filename == 'ticket.html':
22
+ self.log.debug("TicketWebUiAddon not the correct template")
23
+ return stream
24
+ stream = stream | Transformer('//div[@id="banner"]').before(
25
+ tag.script(type="text/javascript",
26
+ src=req.href.chrome("Billing", "ticket.js"))()
27
+ )
28
+ return stream
@@ -0,0 +1,127 @@
1
+ user_manual_title = "Timing and Estimation Plugin User Manual"
2
+ user_manual_version = 13
3
+ user_manual_wiki_title = "TimingAndEstimationPluginUserManual"
4
+ user_manual_content = """
5
+
6
+ [[PageOutline]]
7
+ = Timing and Estimation Plugin User Manual =
8
+ [http://trac-hacks.org/wiki/TimingAndEstimationPlugin TimingAndEstimationPlugin on TracHacks] | [http://trac-hacks.org/report/9?COMPONENT=TimingAndEstimationPlugin Open Tickets] | [http://trac-hacks.org/newticket?component=TimingAndEstimationPlugin&owner=bobbysmith007 New Ticket] |
9
+ [http://trac-hacks.org/browser/timingandestimationplugin/trunk Web Browsable Source Code]
10
+
11
+ == Abstract Design Goal ==
12
+ My goal in writing this plugin was to use as much of the existing structure as possible (therefore not needing to add extra structure that might make maintainability difficult). The largest downside I have found to this is that there is no way to attach more permissions to anything.
13
+
14
+ == Custom Ticket Fields ==
15
+ In adhering to our design goal, rather than creating a new ticket interface, I create some custom fields and a small daemon to watch over them.
16
+
17
+ === Fields: ===
18
+ * '''Hours to Add''' This field functions as a time tracker. When you add hours to it , those hours get added to the total hours field. The person who made the change is there fore credited with the hours spent on it.
19
+ * '''Total Hours''' This field is the total number of hours that have been added to the project. This has been made uneditable by including javascript which replaces the input box with a span containing its value
20
+ * Reports might not agree with each other if this is manually edited (which is possible if you disable javascript).
21
+ * '''Is this billable?''' An extra flag on tickets so that they can be marked as billable / not billable.
22
+ * '''Estimated Hours''' a field that contains the estimated amount of work
23
+ === Future Fields ===
24
+ * '''Ticket Rate''' The ability to attach a cost per hour or total amount to an individual ticket
25
+
26
+ == Billing / Management Page / Time Reports ==
27
+ This page provide a small interface for querying the tickets and adding a bill date at the current time.
28
+ This interface mostly just gives you links that match the interface to open any of the give reports,
29
+ providing it the correct set of input parameters
30
+
31
+ The direct url is '/Billing'.
32
+
33
+ === No Permissions Branch ===
34
+ The 'Management' button should be in the main title bar. It is possible that if you are viewing at a low resolution, it was pushed off the edge of the screen. Also if you are not logged in with report_view permissions, it will not show that button.
35
+
36
+ === Permissions Branch ===
37
+ The 'Time Reports' button should be in the main title bar. Whether or not you see this will be based on whether your user has TIME_VIEW permissions.
38
+
39
+
40
+
41
+
42
+ === Set Bill Date ===
43
+
44
+ This button will add now as a bill date. This is mostly to make it
45
+ easier to select the last time you billed. This would allow you to
46
+ set a ticket as having been billed at a given time while others have
47
+ not, and accurately get the correct billing information for all
48
+ tickets.
49
+
50
+ == Reports ==
51
+ === Report Types ===
52
+ We provide a few different reports for querying different types of data:
53
+ * '''Billing Reports''' Currently the billing reports are the only time based reports, and are therefore useful for getting an estimate what tickets had times (and totals), and which developers spent their time where.
54
+ * Ticket Work Summary
55
+ * Milestone Work Summary
56
+ * Developer Work Summary
57
+ * '''Ticket/Hour Reports''' These reports are useful for reviewing estimates on a large scale or getting an idea of the project at large. These reports currently ignore the time.
58
+ * Ticket Hours
59
+ * Ticket Hours with Description
60
+ * Ticket Hours Grouped By Component
61
+ * Ticket Hours Grouped By Component with Description
62
+ * Ticket Hours Grouped By Milestone
63
+ * Ticket Hours Grouped By Milestone with Description
64
+ === Adding More Reports ===
65
+ To add reports to the Management screen sections, you must run the following sql against your trac database
66
+ Remember to fill in the @reportID of the report you want to insert, and to select the insert statement for the section of your choice.
67
+ * {{{INSERT INTO custom_report (id, uuid, maingroup, subgroup, version, ordering) VALUES (@reportID , @uuid, 'Timing and Estimation Plugin', 'Billing Reports', 1, 0);}}}
68
+ * {{{INSERT INTO custom_report (id, uuid, maingroup, subgroup, version, ordering) VALUES (@reportID , @uuid, 'Timing and Estimation Plugin', 'Ticket/Hour Reports', 1, 0);}}}
69
+
70
+ ''NB: @uuid is a globally uninque identifier created via a tool such as {{{uuidgen}}} on Linux or various [http://www.famkruithof.net/uuid/uuidgen online tools]. It is used in this plugin to provide programatic reference to specific reports such that they can be upgraded successfully on future revisions of the plugin''
71
+
72
+ === Removing a Report ===
73
+ To remove reports from the Management page, run the following query.
74
+ Remember to fill in the @reportID of the report you want to modify.
75
+ * To remove for this version of the plugin (will be over written in future plugin upgrades)
76
+ * {{{UPDATE custom_report SET maingroup='x'||maingroup WHERE report = @reportID;}}}
77
+ * To remove permanently (wont be over written in future plugin upgrades)
78
+ * {{{UPDATE custom_report SET version=9999, maingroup='x'||maingroup WHERE report = @reportID;}}}
79
+ ''NB: The 'x' part is not important - you just need to make the column read something other than 'Timing and Estimation Plugin'.''
80
+
81
+ === TAKE NOTE ===
82
+ '''The reports can only be called from the Management Page. They will not work from the Trac View Tickets page. (Due to the custom variables that need values).'''
83
+
84
+ == Permissions Branch ==
85
+ Recently a branch of this plugin was sponsored by [http://www.obsidiansoft.com/ Obsidian Software] so that it would support per field permissions.
86
+
87
+ This is accomplished with Genshi 5 stream filters in trac 11. This code draws from the [http://trac-hacks.org/wiki/BlackMagicTicketTweaksPlugin BlackMagicTicketTweaksPlugin]
88
+
89
+ === Configuration ===
90
+ There is a new trac.ini configuration section which is filled in by default as follows.
91
+ {{{
92
+ #!ini
93
+ [field settings] # per field permissions
94
+
95
+ # a list of all the fields to apply permissions to
96
+ fields = billable, totalhours, hours, estimatedhours
97
+
98
+ # a bunch of:
99
+ # field.permission = PERMISSION:consequence
100
+ # where consequence is one of: hide, remove, disable
101
+ # hide - replaces with hidden input
102
+ # remove - removes element
103
+ # disable - removes input in favor of text
104
+ billable.permission = TIME_VIEW:hide, TIME_RECORD:disable
105
+ totalhours.permission = TIME_VIEW:remove, TIME_RECORD:disable
106
+ hours.permission = TIME_RECORD:remove
107
+ estimatedhours.permission = TIME_RECORD:disable
108
+ }}}
109
+
110
+
111
+ It also adds an "Internal" checkbox which allows you to set a ticket as internal. For this policy to work correctly you need to add a line to the trac section of the config telling it which permission policies to use. (The setup will attempt to put this line of configuration in place. ) The permission that looks at currently is 'TIME_ADMIN'. To change that group set the internalgroup of the ticket section in the trac.ini as follows:
112
+
113
+ {{{
114
+ #!ini
115
+ [ticket]
116
+ internalgroup = TRAC_ADMIN
117
+
118
+ [trac]
119
+ permission_policies = InternalTicketsPolicy, DefaultPermissionPolicy, LegacyAttachmentPolicy
120
+ }}}
121
+
122
+
123
+ == Future Improvements ==
124
+ * [http://trac-hacks.org/wiki/TimingAndEstimationPlugin See tickets] at the [http://trac-hacks.org/wiki/TimingAndEstimationPlugin project trac]
125
+
126
+
127
+ """
@@ -0,0 +1,129 @@
1
+ from pkg_resources import resource_filename
2
+ import re
3
+ import time
4
+ import datetime
5
+ import dbhelper
6
+ from sets import Set
7
+ from usermanual import *
8
+ from trac.log import logger_factory
9
+ from trac.core import *
10
+ from trac.web import IRequestHandler
11
+ from trac.perm import IPermissionRequestor
12
+ from trac.util import Markup
13
+ from trac.web.chrome import add_stylesheet, add_script, \
14
+ INavigationContributor, ITemplateProvider
15
+ from trac.web.href import Href
16
+ from reportmanager import CustomReportManager
17
+ from statuses import get_statuses
18
+
19
+ #get_statuses = api.get_statuses
20
+
21
+ class TimingEstimationAndBillingPage(Component):
22
+ implements(IPermissionRequestor, INavigationContributor, IRequestHandler, ITemplateProvider)
23
+
24
+ def __init__(self):
25
+ pass
26
+
27
+ # IPermissionRequestor methods
28
+ def get_permission_actions(self):
29
+ return ["TIME_VIEW", "TIME_RECORD", ("TIME_ADMIN", ["TIME_RECORD", "TIME_VIEW"])]
30
+
31
+ def set_bill_date(self, username="Timing and Estimation Plugin", when=0):
32
+ now = time.time()
33
+ if not when:
34
+ when = now
35
+ when = int(when)
36
+ now = int(now)
37
+ dtwhen = datetime.datetime.fromtimestamp(when);
38
+ strwhen = "%s-%s-%s %#02d:%#02d:%#02d" % \
39
+ (dtwhen.year, dtwhen.month, dtwhen.day, dtwhen.hour,dtwhen.minute, dtwhen.second)
40
+ sql = """
41
+ INSERT INTO bill_date (time, set_when, str_value)
42
+ VALUES (%s, %s, %s)
43
+ """
44
+ dbhelper.execute_non_query(self, sql, when, now, strwhen)
45
+
46
+
47
+
48
+
49
+
50
+
51
+ # INavigationContributor methods
52
+ def get_active_navigation_item(self, req):
53
+ val = re.search('/Billing$', req.path_info)
54
+ if val and val.start() == 0:
55
+ return "Billing"
56
+ else:
57
+ return ""
58
+
59
+ def get_navigation_items(self, req):
60
+ url = req.href.Billing()
61
+ if req.perm.has_permission("TIME_VIEW"):
62
+ yield 'mainnav', "Billing", \
63
+ Markup('<a href="%s">%s</a>' % \
64
+ (url , "Time Reports"))
65
+
66
+ # IRequestHandler methods
67
+ def set_request_billing_dates(self, data):
68
+ billing_dates = []
69
+ billing_time_sql = """
70
+ SELECT DISTINCT time as value, str_value as text
71
+ FROM bill_date
72
+ """
73
+ rs = dbhelper.get_result_set(self, billing_time_sql)
74
+ if rs:
75
+ for (value, text) in rs.rows:
76
+ billing_info = {'text':text , 'value':value}
77
+ billing_dates.extend([billing_info])
78
+ #self.log.debug("bill-dates: %s"%billing_dates)
79
+ data['billing_info']["billdates"] = billing_dates
80
+
81
+ def match_request(self, req):
82
+ val = re.search('/Billing$', req.path_info)
83
+ return val and val.start() == 0
84
+
85
+ def process_request(self, req):
86
+ messages = []
87
+ req.perm.require("TIME_VIEW")
88
+ def addMessage(s):
89
+ messages.extend([s]);
90
+
91
+ if req.method == 'POST':
92
+ req.perm.require("TIME_VIEW")
93
+ if req.args.has_key('setbillingtime'):
94
+ self.set_bill_date(req.authname)
95
+ addMessage("All tickets last bill date updated")
96
+
97
+ mgr = CustomReportManager(self.env, self.log)
98
+ data = {};
99
+ data["is_time_admin"] = req.perm.has_permission("TIME_ADMIN")
100
+ data["statuses"] = get_statuses(self)
101
+ data["reports"] = mgr.get_reports_by_group(CustomReportManager.TimingAndEstimationKey);
102
+ #self.log.debug("DEBUG got %s, %s" % (data["reports"], type(data["reports"])));
103
+ data["billing_info"] = {"messages": messages,
104
+ "href": req.href.Billing(),
105
+ "report_base_href": req.href.report(),
106
+ "usermanual_href": req.href.wiki(user_manual_wiki_title),
107
+ "usermanual_title": user_manual_title }
108
+
109
+ self.set_request_billing_dates(data)
110
+
111
+ add_stylesheet(req, "Billing/billingplugin.css")
112
+ add_script(req, "Billing/linkifyer.js")
113
+ return 'billing.html', data, None
114
+
115
+
116
+ # ITemplateProvider
117
+ def get_htdocs_dirs(self):
118
+ """Return the absolute path of a directory containing additional
119
+ static resources (such as images, style sheets, etc).
120
+ """
121
+ return [('Billing', resource_filename(__name__, 'htdocs'))]
122
+
123
+ def get_templates_dirs(self):
124
+ """Return the absolute path of the directory containing the provided
125
+ genshi templates.
126
+ """
127
+ rtn = [resource_filename(__name__, 'templates')]
128
+ return rtn
129
+