source: budgetingplugin/trunk/ticketbudgeting/ticketbudgeting.py

Last change on this file was 17241, checked in by Ryan J Ollos, 5 years ago

0.6.6: Conform to PEP8

Bump version to 0.6.6.

  • Property svn:eol-style set to native
File size: 31.8 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2012-2015 Franz Mayer Gefasoft AG
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution.
8
9import locale
10import re
11import time
12from pkg_resources import resource_filename
13
14from genshi import HTML
15from genshi.filters import Transformer
16from trac.config import Option, IntOption
17from trac.core import Component, implements
18from trac.db.api import DatabaseManager
19from trac.db.schema import Table, Column
20from trac.perm import IPermissionRequestor
21from trac.ticket.api import ITicketManipulator
22from trac.ticket.model import Ticket
23from trac.util import exception_to_unicode
24from trac.util.html import html
25from trac.util.translation import domain_functions
26from trac.web.api import ITemplateStreamFilter, IRequestFilter
27from trac.web.chrome import ITemplateProvider, add_script
28
29_, tag_, N_, add_domain = domain_functions('ticketbudgeting', '_',
30                                           'tag_', 'N_', 'add_domain')
31
32BUDGETING_TABLE = Table('budgeting', key=('ticket', 'position'))[
33    Column('ticket', type='int'),
34    Column('position', type='int'),
35    Column('username'),
36    Column('type'),
37    Column('estimation', type='int64'),
38    Column('cost', type='int64'),
39    Column('status', type='int'),
40    Column('comment')
41]
42
43BUDGET_REPORT_ALL_ID = 90
44
45authorizedToModify = ['TICKET_MODIFY', 'TRAC_ADMIN',
46                      'TICKET_BUDGETING_MODIFY']
47
48_VALUE_NAMES = 'username,type,estimation,cost,status,comment'
49_VALUE_NAMES_LIST = _VALUE_NAMES.split(',')
50
51
52def get_float(value, fld='UNKNOWN FIELD'):
53    try:
54        if not value:
55            ret_val = 0
56        else:
57            try:
58                ret_val = locale.atof(value)
59            except:
60                str(value).replace(',', '.')
61                ret_val = float(value)
62    except Exception, e:
63        fld = '%s.%s' % (BUDGETING_TABLE.name, fld)
64        raise Exception(fld, e)
65    return ret_val
66
67
68class Budget:
69    """ Container class for budgeting info"""
70    _action = None
71    _values = None
72    _diff = None
73
74    def __init__(self):
75        self._values = {}
76
77    def set(self, number, value):
78        if number is None:
79            return
80
81        number = int(number)
82        if 0 < number < len(_VALUE_NAMES_LIST) + 1:
83            fld = _VALUE_NAMES_LIST[number - 1]
84
85            if fld == 'status':
86                try:
87                    if value == '':
88                        self._values[fld] = 0
89                    else:
90                        self._values[fld] = int(value)
91                except Exception, e:
92                    fld = '%s.%s' % (BUDGETING_TABLE.name, fld)
93                    raise Exception(fld, e)
94            elif fld in ('estimation', 'cost'):
95                self._values[fld] = get_float(value, fld)
96            else:
97                self._values[fld] = value
98
99    def do_action(self, env, ticket_id, position, def_cost=0):
100        if not self._action:
101            env.log.warn('no action defined!')
102            return
103
104        self._diff = {}
105
106        if not ticket_id or not position:
107            env.log.error('no ticket-id or position available!')
108
109        elif self._action == "Insert":
110            set_attrs = 'ticket,position'
111            set_vals_space = '%s,%s'
112            set_vals = [ticket_id, position]
113
114            for key, value in self._values.iteritems():
115                if key in ('username', 'type', 'comment'):
116                    value = value.encode("utf-8")
117                elif key == 'cost' and def_cost == -1:
118                    value = 0
119
120                set_attrs += ",%s" % key
121                set_vals_space += ",%s"
122                set_vals.append(value)
123
124            self._diff[''] = (self._to_str(), '')
125            sql = ("INSERT INTO %s (%s) VALUES (%s)" %
126                   (BUDGETING_TABLE.name, set_attrs, set_vals_space))
127            env.db_transaction(sql, set_vals)
128            env.log.debug("Added Budgeting-row at position %s to ticket %s:"
129                          "\n%s", position, ticket_id, self._to_str())
130
131        elif self._action == "Update":
132
133            old_values_sql = ("SELECT %s FROM %s WHERE"
134                              " ticket=%%s AND position =%%s"
135                              % (_VALUE_NAMES, BUDGETING_TABLE.name))
136            old_values = env.db_query(old_values_sql,
137                                      (ticket_id, position))[0]
138
139            set_attrs = ''
140            set_vals = []
141
142            for attrnr in range(len(_VALUE_NAMES_LIST)):
143                value = self.get_value(attrnr + 1)
144                key = _VALUE_NAMES_LIST[attrnr]
145
146                if key == 'cost' and def_cost == -1:
147                    continue
148                elif key in ('username', 'type', 'comment'):
149                    value = value.encode("utf-8")
150                elif key in ('estimation', 'cost', 'status'):
151                    value = 0 if value == '' or get_float(value) == 0 \
152                              else value
153
154                if not old_values[attrnr] == value:
155                    new = '%s: %s' % (key, value)
156                    old = '%s: %s' % (key, old_values[attrnr])
157
158                    if not set_attrs == '':
159                        set_attrs += ','
160
161                    set_attrs += '%s=%%s' % key
162                    set_vals.append(str(value))
163
164                    self._diff[".%s" % key] = (new, old)
165
166            if not set_attrs == '':
167                sql = ("UPDATE %s SET %s WHERE ticket=%%s AND position=%%s"
168                       % (BUDGETING_TABLE.name, set_attrs))
169
170                set_vals.append(ticket_id)
171                set_vals.append(position)
172                env.db_transaction(sql, set_vals)
173                env.log.debug("Updated Budgeting-row for ticket %s"
174                              " at position %s:\n%s",
175                              ticket_id, position, self._to_str())
176
177        elif self._action == "Delete":
178            self._diff[''] = ('', self._to_str())
179
180            sql = ("DELETE FROM %s WHERE ticket=%%s AND position=%%s"
181                   % BUDGETING_TABLE.name)
182            env.db_transaction(sql, (ticket_id, position))
183
184            env.log.debug("Deleted Budgeting-row for ticket %s"
185                          " at position %s:\n%s",
186                          ticket_id, position, self._to_str())
187        else:
188            env.log.error('no appropriate action found! _action is: %s',
189                          self._action)
190
191    def get_values(self):
192        return self._values
193
194    def _to_str(self):
195        return ("username: %s, type: %s, estimation: %s, cost: %s,"
196                " state: %s, comment: %s" %
197                (self.get_value(1), self.get_value(2), self.get_value(3),
198                 self.get_value(4), self.get_value(5), self.get_value(6)))
199
200    def get_value(self, number):
201        if number is None:
202            return ""
203
204        number = int(number)
205        if 0 < number < _VALUE_NAMES_LIST.__len__() + 1:
206            fld = _VALUE_NAMES_LIST[number - 1]
207            if fld in 'estimation':
208                return locale.format('%.2f', self._values[fld])
209            elif fld in 'cost':
210                if 'cost' in self._values:
211                    return locale.format('%.2f', self._values[fld])
212                else:
213                    return locale.format('%.2f', 0)
214            return self._values[fld]
215        return ""
216
217    def set_action(self, action):
218        self._action = action
219
220    def get_diff(self):
221        return self._diff
222
223
224class TicketBudgetingView(Component):
225    implements(ITicketManipulator, ITemplateProvider, ITemplateStreamFilter,
226               IRequestFilter)
227
228    _CONFIG_SECTION = 'budgeting-plugin'
229
230    # these options won't be saved to trac.ini
231    _types = Option(
232        _CONFIG_SECTION, 'types',
233        'Implementation|Documentation|Specification|Test',
234        """Types of work, which could be selected in select-box.""")
235
236    Option(
237        _CONFIG_SECTION, 'retrieve_users', "permission",
238        """indicates whether users should be retrieved from session or
239        permission table; possible values: permission, session""")
240
241    Option(
242        _CONFIG_SECTION, 'exclude_users',
243        "'anonymous','authenticated','tracadmin'",
244        """list of users, which should be excluded to show in the
245        drop-down list; should be usable as SQL-IN list""")
246
247    _def_cost = IntOption(
248        _CONFIG_SECTION, 'default_cost', 0,
249        doc="""Default costs or -1 to disabled entering costs.
250               This might useful when costs are entered by third
251               party software.""")
252
253    _type_list = None
254    _name_list = None
255    _name_list_str = None
256    _budgets = None
257    _changed_by_author = None
258
259    BUDGET_REPORTS = [
260        (BUDGET_REPORT_ALL_ID, 'report_title_90', 'report_description_90',
261         """SELECT t.id, t.summary,
262                   t.milestone AS __group__, '../milestone/' ||
263                   t.milestone AS __grouplink__, t.owner, t.reporter,
264                   t.status, t.type, t.priority, t.component,
265                   COUNT(b.ticket) AS Count, SUM(b.cost) AS Cost,
266                   SUM(b.estimation) AS Effort,
267                   %(status)s || '%%' AS "Status",
268          (CASE
269            WHEN t.status='closed'
270            THEN 'color: #777; background: #ddd; border-color: #ccc;'
271            WHEN SUM(b.cost) > SUM(b.estimation)
272            THEN 'font-weight: bold; background: orange;'
273           END) AS __style__
274         FROM ticket t
275         LEFT JOIN budgeting b ON b.ticket = t.id
276         WHERE t.milestone LIKE
277         (CASE $MILESTONE
278                   WHEN '' THEN '%%'
279                   ELSE $MILESTONE END) and
280         (t.component LIKE (CASE $COMPONENT
281                   WHEN '' THEN '%%'
282                   ELSE $COMPONENT END) or t.component is null) and
283         (t.owner LIKE (CASE $OWNER
284                   WHEN '''' THEN $USER
285                   ELSE $OWNER END) or t.owner is null or
286          b.username LIKE (CASE $OWNER
287                   WHEN '''' THEN $USER
288                   ELSE $OWNER END) )
289         GROUP BY t.id, t.type, t.priority, t.summary, t.owner, t.reporter,
290                  t.component, t.status, t.milestone
291         HAVING COUNT(b.ticket) > 0
292         ORDER BY t.milestone DESC, t.status, t.id DESC
293         """)]
294
295    def __init__(self):
296        try:
297            locale_dir = resource_filename(__name__, 'locale')
298        except KeyError:
299            pass
300        else:
301            add_domain(self.env.path, locale_dir)
302
303        try:
304            self.env.db_query("SELECT ticket FROM %s where ticket is null" %
305                              BUDGETING_TABLE.name)
306            self.log.debug("[TicketBudgetingView.__init__] table '%s' "
307                           "already exists", BUDGETING_TABLE.name)
308        except Exception:
309            self.log.debug("[TicketBudgetingView.__init__] table '%s' "
310                           "does not exists", BUDGETING_TABLE.name)
311            try:
312                self.create_table()
313                self.log.info("[__init__] table '%s' successfully created",
314                              BUDGETING_TABLE.name)
315                self.create_reports()
316                self.log.info("[__init__] report '%s' successfully created",
317                              BUDGET_REPORT_ALL_ID)
318            except Exception, e:
319                self.log.error("[__init__] ERROR when creating table"
320                               " or report: %s", e)
321
322    def filter_stream(self, req, method, filename, stream, data):
323        if filename == 'ticket.html' and data:
324            tkt = data['ticket']
325            if tkt and tkt.id:
326                self._load_budget(tkt.id)
327            else:
328                self._budgets = {}
329
330            input_html, preview_html = self._get_ticket_html()
331
332            modify_allowed = False
333            for perm in authorizedToModify:
334                modify_allowed = modify_allowed or \
335                                 perm in req.perm(tkt.resource)
336
337            if modify_allowed:
338                visibility = ' style="display: none"'
339                if self._budgets:
340                    visibility = ''
341
342                fieldset = self._get_budget_fieldset() \
343                           % (visibility, input_html)
344                stream |= Transformer('.//fieldset [@id="properties"]')\
345                          .after(HTML(fieldset))
346
347                # Load default values for Type, Estimation, Cost an State
348                # from trac.ini
349                def_type = self._get_budget_attr('default_type')
350                if not def_type:
351                    # If the configured default-type is not available,
352                    # submit -1 ==> first element in type list will be
353                    # selected
354                    def_type = '-1'
355                def_est = self._get_budget_attr('default_estimation')
356                if not def_est:
357                    def_est = '0.0'
358
359                def_state = self._get_budget_attr('default_state')
360                if not def_state:
361                    def_state = '0'
362
363                defaults = html.div(
364                    html.a(self._type_list, id="selectTypes"),
365                    html.a(self._name_list_str, id="selectNames"),
366                    html.a(req.authname, id="def_name"),
367                    html.a(def_type, id="def_type"),
368                    html.a(def_est, id="def_est"),
369                    html.a(self._def_cost, id="def_cost"),
370                    html.a(def_state, id="def_state"),
371                    style="display: none")
372
373                stream |= Transformer('.//fieldset [@id="budget"]')\
374                          .append(defaults)
375
376            if preview_html:
377                fieldset_str = self._get_budget_preview() % preview_html
378                stream |= Transformer('//div [@id="content"]'
379                                      '//div [@id="ticket"]')\
380                          .after(HTML(fieldset_str))
381        elif filename == 'milestone_view.html':
382            by = 'component'
383            if 'by' in req.args:
384                by = req.args['by']
385            budget_stats, stats_by = self._get_milestone_html(req, by)
386            stats_by = u"<fieldset><legend>Budget</legend>" \
387                       u"<table>%s</table></fieldset>" % stats_by
388            stream |= Transformer('//form[@id="stats"]')\
389                      .append(HTML(stats_by))
390            stream |= Transformer('//div[@class="info"]')\
391                      .append(HTML(budget_stats))
392        return stream
393
394    def _get_budget_attr(self, name):
395        return self.config.get('budgeting-plugin', name)
396
397    def _get_budget_fieldset(self):
398        title = _('in hours')
399        fieldset = ('<fieldset id="budget">'
400                    '<legend>' + _('Budget Estimation') + '</legend>'
401                    '<div class="inlinebuttons">'
402                    '<label>' + _('Add a new row') + '</label>'
403                    '<input type="button" name="addRow" style="margin-left: 5px; border-radius: 1em 1em 1em 1em; font-size: 100%%" onclick="addBudgetRow()" value = "&#x271A;"/>'
404                    '</div>'
405                       '<span id="hiddenbudgettable"%s>'
406                       '<table>'
407                       '<thead id="budgethead">'
408                       '<tr>'
409                            '<th>' + _('Person') + '</th>'
410                            '<th>' + _('Type') + '</th>'
411                            '<th title="' + title + '">' + _('Estimation') + '</th>' +
412                            ('<th title="' + title + '">' + _('Cost') + '</th>' if self._def_cost != -1 else '') +
413                            '<th>' + _('State') + '</th>'
414                            '<th style="width:300px">' + _('Comment') + '</th>'
415                        '</tr>'
416                        '</thead>'
417                        '<tbody id="budget_container">%s</tbody>'
418                        '</table>'
419                        '</span>'
420                        '</fieldset>')
421        self.log.debug('fieldset: %s', fieldset)
422        return fieldset
423
424    def _get_budget_preview(self):
425        fieldset = (
426            '<div id="budgetpreview">'
427            '<h2 class="foldable">' + _('Budget Estimation') + '</h2>'
428            '<table class="listing">'
429            '<thead>'
430              '<tr>'
431                '<th style="width:90px">' + _('Person') + '</th>'
432                '<th style="width:90px">' + _('Type') + '</th>'
433                '<th style="width:90px">' + _('Estimation') + '</th>'
434                '<th style="width:90px">' + _('Cost') + '</th>'
435                '<th style="width:90px">' + _('State') + '</th>'
436                '<th style="width:300px">' + _('Comment') + '</th>'
437              '</tr>'
438            '</thead>'
439            '<tbody id="previewContainer">%s'
440            '</tbody>'
441            '</table>'
442            '</div>')
443        return fieldset
444
445    def pre_process_request(self, req, handler):
446        """ overridden from IRequestFilter"""
447        return handler
448
449    def post_process_request(self, req, template, data, content_type):
450        """ overridden from IRequestFilter"""
451        if req.path_info.startswith('/newticket') or \
452                req.path_info.startswith('/ticket'):
453            add_script(req, 'hw/js/budgeting.js')
454            if not data:
455                return template, data, content_type
456            tkt = data['ticket']
457
458            if tkt and tkt.id and Ticket.id_is_valid(tkt.id):
459                # ticket is ready for saving
460                if self._changed_by_author:
461                    self._save_budget(tkt)
462                self._budgets = None
463        return template, data, content_type
464
465    def _get_fields(self, req):
466        budget_dict = {}
467        # searching budget fields and send them to db
468        for arg in req.args:
469            field_attrs = arg.split("-")
470            if len(field_attrs) >= 2:
471                row_no = field_attrs[0]
472                if row_no in budget_dict:
473                    budget_obj = budget_dict[row_no]
474                else:
475                    budget_obj = Budget()
476                    budget_dict[row_no] = budget_obj
477                budget_obj.set(field_attrs[1], req.args.get(arg))
478
479                if len(field_attrs) == 3:
480                    # New created field, should be insered
481                    if field_attrs[2] in ('Insert', 'Delete', 'Update'):
482                        budget_obj.set_action(field_attrs[2])
483        return budget_dict
484
485    def _get_milestone_html(self, req, group_by):
486        html = ''
487        stats_by = ''
488        ms = req.args['id']
489
490        for row in self.env.db_query("""
491                SELECT SUM(b.cost), SUM(b.estimation), AVG(b.status)
492                FROM budgeting b, ticket t
493                WHERE b.ticket=t.id AND t.milestone=%s
494                """, (ms,)):
495            html = '<dl><dt>' + _('Budget in hours') + ':</dt><dd> </dd>' \
496                   '<dt>' + _('Cost') + ': <dd>%.2f</dd></dt>' \
497                   '<dt>' + _('Estimation') + ': <dd>%.2f</dd></dt>' \
498                   '<dt>' + _('Status') + ': <dd>%.1f%%</dd></dt></dl>'
499            html %= row[0], row[1], row[2]
500            html = self._get_progress_html(row[0], row[1], row[2]) + html
501
502        if not group_by:
503            return html, stats_by
504
505        for row in self.env.db_query("""
506                SELECT t.%s, SUM(b.cost), SUM(b.estimation), AVG(b.status)
507                FROM budgeting b, ticket t
508                WHERE b.ticket=t.id AND t.milestone=%%s
509                GROUP BY t.%s ORDER BY t.%s
510                """ % (group_by, group_by, group_by), (ms,)):
511            status_bar = self._get_progress_html(row[1], row[2], row[3], 75)
512            link = req.href.query({'milestone': ms, group_by: row[0]})
513            if group_by == 'component':
514                link = req.href.report(BUDGET_REPORT_ALL_ID,
515                                       {'MILESTONE': ms,
516                                        'COMPONENT': row[0],
517                                        'OWNER': '%'})
518
519            stats_by += '<tr><th scope="row"><a href="%s">' \
520                        '%s</a></th>' % (link, row[0])
521            stats_by += '<td>%s</td></tr>' % status_bar
522
523        return html, stats_by
524
525    def _get_progress_html(self, cost, estimation, status, width=None):
526        ratio = 0
527        if estimation > 0 and cost:
528            left_bar_value = int(round((cost * 100) / estimation, 0))
529            ratio = left_bar_value
530            right_bar_value = int(round(100 - left_bar_value, 0))
531            if right_bar_value + left_bar_value < 100:
532                right_bar_value += 1
533            elif left_bar_value > 100:
534                left_bar_value = int(100)
535                right_bar_value = int(0)
536        else:
537            left_bar_value = int(0)
538            right_bar_value = int(100)
539
540        style_cost = "width: " + str(left_bar_value) + "%"
541        style_est = "width: " + str(right_bar_value) + "%"
542        title = ' title="' + _('Cost') + ' / ' + _('Estimation') + \
543                ': %.1f / %.1f (%.0f %%); ' + _('Status') + ': %.1f%%"'
544        title %= cost, estimation, ratio, status
545        right_legend = "%.0f %%" % ratio
546
547        if int(status) == 100:
548            style_cost += ";background:none repeat scroll 0 0 #3300FF;"
549            style_est += ";background:none repeat scroll 0 0 #00BB00;"
550        elif ratio > 100:
551            style_cost += ";background:none repeat scroll 0 0 #BB0000;"
552
553        status_bar = '<table class="progress"'
554        if width:
555            status_bar += ' style="width: ' + str(width) + '%"'
556            right_legend = "%.0f / %.0f" % (cost, estimation)
557        status_bar += '><tr><td class="closed" style="' + style_cost + '">\
558               <a' + title + '></a> \
559               </td><td style="' + style_est + '" class="open">\
560               <a' + title + '></a> \
561               </td></tr></table><p class="percent"' + title + '>' + \
562               right_legend + '</p>'
563
564        return status_bar
565
566    def _get_ticket_html(self):
567        input_html = ''
568        preview_html = ''
569
570        if not self._type_list:
571            types_str = self.config.get(self._CONFIG_SECTION, 'types')
572            self._type_list = re.sub(r'\|', ';', types_str)
573            self.log.debug("INIT self._type_list: %s", self._type_list)
574        types = self._type_list.split(';')
575
576        if not self._name_list:
577            self._name_list = self.get_user_list()
578            self.log.debug("INIT self._name_list: %s", self._name_list)
579            for user in self._name_list:
580                if not self._name_list_str:
581                    self._name_list_str = str(user)
582                else:
583                    self._name_list_str += ';' + str(user)
584
585        if self._budgets:
586            for pos, budget in self._budgets.iteritems():
587                user_options = ''
588                type_options = ''
589                values = budget.get_values()
590                input_html += '<tr id="row-%s">' % pos
591                preview_html += '<tr>'
592                el_in_list = False
593
594                if self._name_list:
595                    for opt in self._name_list:
596                        selected = ''
597                        if values['username'] == opt:
598                            selected = ' selected'
599                            el_in_list = True
600#                            preview_html += '<td>%s</td>' % opt
601                        user_options += '<option%s>%s</option>' \
602                                        % (selected, opt)
603                if not el_in_list:
604                    user_options += '<option selected>%s</option>' \
605                                    % (values['username'])
606
607                el_in_list = False
608                for t in types:
609                    selected = ''
610                    if values['type'] == t:
611                        selected = ' selected'
612                        el_in_list = True
613#                        preview_html += '<td>%s</td>' % t
614                    type_options += '<option%s>%s</option>' % (selected, t)
615                if not el_in_list:
616                    type_options += '<option selected>%s</option>' \
617                                    % (values['type'])
618
619                input_html += '<td><select onChange="update(%s,1)" ' \
620                              'name="%s-1" >%s</select></td>' \
621                              % (pos, pos, user_options)
622                preview_html += '<td>%s</td>' % values['username']
623                input_html += '<td><select onChange="update(%s,2)" ' \
624                              'name="%s-2">%s</select></td>' \
625                              % (pos, pos, type_options)
626                preview_html += '<td>%s</td>' % values['type']
627                size = 10
628                for col in range(3, 7):
629                    col_val = budget.get_value(col)
630                    if col == 6 and col_val:  # comment
631                        col_val = col_val.replace('"', "&quot;")
632                        size = 60
633                    elif not col_val:
634                        if col < 6:
635                            col_val = '0'
636                        else:
637                            col_val = ''
638                            size = 60
639
640                    if col == 4 and self._def_cost == -1:  # disable cost
641                        input_html += '<input type="hidden" name="%s-%s" value="%s" />' % (pos, col, 0)
642                    else:
643                        input_html += '<td><input size="%s" onChange="update(%s,%s)" name="%s-%s" value="%s"></td>' % (size, pos, col, pos, col, col_val)
644                    preview_html += '<td>%s' % col_val
645                    if col == 5:
646                        preview_html += '&nbsp;%'
647                    preview_html += '</td>'
648                input_html += '<td><div class="inlinebuttons"><input type="button" style="border-radius: 1em 1em 1em 1em; font-size: 100%%" name="deleteRow%s" onclick="deleteRow(%s)" value = "&#x2718;"/></div></td>' % (pos, pos)
649                input_html += '</tr>'
650                preview_html += '</tr>'
651        return input_html, preview_html
652
653    # ITemplateProvider methods
654
655    def get_templates_dirs(self):
656        return [resource_filename(__name__, 'htdocs')]
657
658    def get_htdocs_dirs(self):
659        return [('hw', resource_filename(__name__, 'htdocs'))]
660
661    def _load_budget(self, ticket_id):
662        self._budgets = {}
663        if not ticket_id:
664            return
665
666        for row in self.env.db_query("""
667                SELECT position,username,type,estimation,cost,status,comment
668                FROM budgeting WHERE ticket=%s ORDER BY position
669                """, (ticket_id,)):
670            budget = Budget()
671            for i, col in enumerate(row):
672                if i > 0:
673                    budget.set(i, col)
674            pos = int(row[0])
675            self._budgets[pos] = budget
676            self.log.debug("[_load_budget] loaded budget: %s",
677                           budget.get_values())
678
679    def _save_budget(self, tkt):
680        if self._budgets and tkt and tkt.id:
681            user = self._changed_by_author
682            self._changed_by_author = None
683            for pos, budget in self._budgets.iteritems():
684                budget.do_action(self.env, tkt.id, int(pos), self._def_cost)
685                self.log.debug("saved budget of position: %s", pos)
686            self._log_changes(tkt, user)
687            self._budgets = None
688
689    def _log_changes(self, tkt, change_user):
690        if not tkt or not tkt.id:
691            return
692        cur_time = self._get_current_time()
693
694        try:
695            for pos, budget in self._budgets.iteritems():
696                if budget.get_diff():
697                    diff = budget.get_diff()
698                    for key, (new, old) in diff.iteritems():
699                        sql = ("INSERT INTO ticket_change "
700                               "(ticket,time,author,field,oldvalue,newvalue) "
701                               "VALUES(%s,%s,%s,%s,%s,%s)")
702                        self.env.db_transaction(sql,
703                            (tkt.id, cur_time, change_user, 'budgeting.%s%s'
704                             % (pos, key), old, new))
705        except Exception, ex:
706            self.log.error("Error while logging change: %s", ex)
707
708    def _get_current_time(self):
709        return (time.time() - 1) * 1000000
710
711    def validate_ticket(self, req, ticket):
712        """ overriden from ITicketManipulator """
713        errors = []
714        try:
715            self._budgets = self._get_fields(req)
716            self._changed_by_author = req.authname or 'anonymous'
717            self.log.info("[validate] budget has changed by author: %s",
718                           self._changed_by_author)
719        except Exception, ex:
720            self.log.error("Error while validating: %s", ex)
721            fld, e = ex
722            errors.append([fld, str(e)])
723
724        return errors
725
726    def create_table(self):
727        """Constructor, see trac/postgres_backend.py:95 (method init_db)
728        """
729        conn, dummy_args = DatabaseManager(self.env).get_connector()
730        try:
731            with self.env.db_transaction as db:
732                for stmt in conn.to_sql(BUDGETING_TABLE):
733                    try:
734                        if db.schema:
735                            stmt = re.sub(r'CREATE TABLE ', 'CREATE TABLE "'
736                                          + db.schema + '".', stmt)
737                    except Exception, e:
738                        self.log.warn("[INIT table] substituting schema "
739                                      "throws error: %s", e)
740                    stmt = re.sub(r'(?i)bigint', 'NUMERIC(10,2)', stmt)
741                    self.log.info("[INIT table] executing sql: %s", stmt)
742                    db(stmt)
743                    self.log.info("[INIT table] successfully created "
744                                  "table %s", BUDGETING_TABLE.name)
745        except Exception, e:
746            self.log.error("[INIT table] Error executing SQL Statement\n"
747                           "%s", e)
748
749    def create_reports(self):
750        for report in self.BUDGET_REPORTS:
751            sql = """
752                INSERT INTO report (id, author, title, query, description)
753                VALUES (%s, NULL, %s, %s, %s)
754            """
755            try:
756                with self.env.db_transaction as db:
757                    query = report[3] \
758                            % {'status': db.cast('AVG(b.status)', 'int')}
759                    db(sql, (report[0], _(report[1]), query, _(report[2])))
760            except Exception, e:
761                self.log.error("[INIT reports] Error executing SQL "
762                               "Statement%s",
763                               exception_to_unicode(e, traceback=True))
764            else:
765                self.log.info("[INIT reports] successfully created report "
766                              "with id %s", report[0])
767
768    def get_col_list(self, ignore_cols=None):
769        """ return col list as string; usable for selecting all cols
770        from budgeting table """
771        col_list = ""
772        i = 0
773        for col in BUDGETING_TABLE.columns:
774            try:
775                if ignore_cols and ignore_cols.index(col.name) > -1:
776                    continue
777            except:
778                pass
779
780            if i > 0:
781                col_list += ","
782            col_list += col.name
783            i += 1
784        return col_list
785
786    def get_user_list(self):
787        sql_result = []
788
789        sql = ("SELECT DISTINCT sid FROM session WHERE authenticated > 0"
790               " ORDER BY sid")
791
792        if self.config.get(self._CONFIG_SECTION, 'retrieve_users') == \
793                'permission':
794            sql = "SELECT DISTINCT username FROM permission"
795            if self.config.get(self._CONFIG_SECTION, 'exclude_users'):
796                excl_user = self.config.get(self._CONFIG_SECTION,
797                                            'exclude_users')
798                sql = "%s WHERE username NOT IN (%s)" % (sql, excl_user)
799            sql += " ORDER BY username"
800        for row in self.env.db_query(sql):
801            sql_result.append(row[0])
802        return sql_result
803
804
805class TicketBudgetingPermission(Component):
806    """Publicise permission TICKET_BUDGETING_MODIFY """
807    implements(IPermissionRequestor)
808
809    definedPermissions = "TICKET_BUDGETING_MODIFY"
810
811    # IPermissionRequestor methods
812
813    def get_permission_actions(self):
814        yield self.definedPermissions
Note: See TracBrowser for help on using the repository browser.