Changeset 4909

Show
Ignore:
Timestamp:
12/01/08 08:13:29 (1 month ago)
Author:
optilude
Message:

Check in work previously in patch

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • estimationtoolsplugin/branches/optilude-change-line/estimationtools/burndownchart.py

    r4733 r4909  
    22from datetime import datetime 
    33from datetime import timedelta 
    4 from estimationtools.utils import parse_options, execute_query, get_estimation_field,\ 
    5     get_closed_states 
     4from estimationtools.utils import parse_options, execute_query 
     5from estimationtools.utils import get_estimation_field, get_closed_states, get_initial_estimation_field 
    66from trac.core import TracError 
    77from trac.util.html import Markup 
     
    1010import copy 
    1111 
    12 DEFAULT_OPTIONS = {'width': '800', 'height': '200', 'color': 'ff9900'} 
     12DEFAULT_OPTIONS = {'width': '800', 'height': '200', 'color': 'ff9900,aa8800'} 
    1313 
    1414class BurndownChart(WikiMacroBase): 
     
    2828     * `color`: color specified as 6-letter string of hexadecimal values in the format `RRGGBB`. 
    2929       Defaults to `ff9900`, a nice orange. 
     30     * `title`: the title of the graph. If omitted, defaults to the title of the first milestone given, if any. 
     31     * `change`: set to 1 to include a second line showing what the burndown would've looked like, had 
     32        there not been any scope change 
     33     * `interval_days`: the number of days between points on the x axis, defaulting to 1 
    3034      
    3135    Examples: 
     
    3741    """ 
    3842 
     43    initial_estimation_field = get_initial_estimation_field() 
    3944    estimation_field = get_estimation_field() 
    4045    closed_states = get_closed_states() 
    4146     
    4247    def render_macro(self, req, name, content): 
    43  
     48         
    4449        # prepare options 
    4550        options, query_args = parse_options(self.env.get_db_cnx(), content, copy.copy(DEFAULT_OPTIONS)) 
     
    5257            options['enddate'] = options['startdate'] + timedelta(days=1) 
    5358 
     59        change = options['change'] 
     60 
    5461        # calculate data 
    55         timetable = self._calculate_timetable(options, query_args, req) 
    56  
    57         # scale data       
    58         xdata, ydata, maxhours = self._scale_data(timetable, options) 
    59      
     62        timetable, delta = self._calculate_timetable(options, query_args, req) 
     63        timetable_less_change = {} 
     64         
     65        if change: 
     66             
     67            cumulative_change = Decimal(0) 
     68            for current_date in sorted(timetable.keys()): 
     69                cumulative_change += delta.get(current_date, Decimal(0)) 
     70                timetable_less_change[current_date] = timetable[current_date] - cumulative_change 
     71 
     72        dates = sorted(timetable.keys()) 
     73 
    6074        # build html for google chart api 
    61         dates = sorted(timetable.keys()) 
    62         bottomaxis = "0:|" + ("|").join([str(date.day) for date in dates]) + \ 
     75 
     76        chart_params = {} 
     77        chart_params['cht'] = 'lxy' 
     78        chart_params['chtt'] = self._get_title(options) 
     79        chart_params['chco'] = options['color'] 
     80        chart_params['chs'] = "%sx%s" % (options['width'], options['height']) 
     81        chart_params['chg'] = "100.0,100.0,1,0"  # create top and right bounding line by using grid" 
     82        chart_params['chxt'] = "x,x,x,y" 
     83         
     84        # Add scaled data 
     85        maxhours = max(timetable.values()) 
     86        if change: 
     87            maxhours = max(maxhours, max(timetable_less_change.values())) 
     88         
     89        xdata, ydata,maxhours = self._scale_data(timetable, dates, maxhours, options) 
     90        chart_params['chd'] = "t:%s|%s" % (",".join(xdata), ",".join(ydata),) 
     91        cxdata = cydata = None 
     92        if change: 
     93            cxdata, cydata,maxhours = self._scale_data(timetable_less_change, dates, maxhours, options) 
     94            chart_params['chd'] += "|%s|%s" % (",".join(cxdata), ",".join(cydata),) 
     95         
     96        if change:             
     97            chart_params['chdl'] = "Current|Less change" 
     98         
     99        # Add axes 
     100        chart_params['chxl'] = "0:|" + "|".join([str(date.day) for date in dates]) + \ 
    63101            "|1:|%s|%s" % (dates[0].month, dates[ - 1].month) + \ 
    64102            "|2:|%s|%s" % (dates[0].year, dates[ - 1].year) 
    65         leftaxis = "3,0,%s" % maxhours 
    66          
    67         # mark weekends 
     103        chart_params['chxr'] = "3,0,%s" % maxhours 
     104         
     105        # Add weekends 
     106        downtime = self._mark_downtime(options, dates, xdata, ydata) 
     107        if downtime: 
     108            chart_params['chm'] = '|'.join(downtime) 
     109         
     110        return Markup("<img src=\"http://chart.apis.google.com/chart?%s\" alt=\"Burndown Chart\" />" %  
     111                        "&amp;".join("%s=%s" % (k,v) for k,v in chart_params.items())) 
     112 
     113    def _calculate_timetable(self, options, query_args, req): 
     114        db = self.env.get_db_cnx() 
     115 
     116        estimation_field = self.estimation_field 
     117        initial_estimation_field = self.initial_estimation_field or estimation_field 
     118        one_field = (estimation_field == initial_estimation_field) 
     119 
     120        # create dictionary with entry for each day of the required time period 
     121        timetable = {} 
     122        delta = {} 
     123         
     124        current_date = options['startdate'] 
     125        max_date = current_date 
     126        while current_date <= options['enddate']: 
     127            timetable[current_date] = Decimal(0) 
     128            delta[current_date] = Decimal(0) 
     129            max_date = current_date 
     130            current_date += timedelta(days=options.get('interval_days', 1)) 
     131 
     132        # Ensure we have today's date in the timetable as well, in case interval_days 
     133        # caused us to stop before it. 
     134        if options['today'] <= options['enddate']: 
     135            timetable[options['today']] = Decimal(0) 
     136            delta[options['today']] = Decimal(0) 
     137 
     138        # get current values for all tickets within milestone and sprints      
     139         
     140        query_args[estimation_field + "!"] = None 
     141        if estimation_field != initial_estimation_field: 
     142            query_args[initial_estimation_field + "!"] = None 
     143        query_args['status' + "!"] = None 
     144         
     145        tickets = execute_query(self.env, req, query_args) 
     146 
     147        # add the open effort for each ticket for each day to the timetable 
     148 
     149        for t in tickets: 
     150             
     151            # Record the current (latest) status and estimate, and ticket 
     152            # creation date 
     153             
     154            creation_date = t['time'].date() 
     155            latest_status = t['status'] 
     156            latest_estimate = self._cast_estimate(t[estimation_field]) 
     157            if not latest_estimate: 
     158                latest_estimate = Decimal(0) 
     159            latest_initial_estimate = self._cast_estimate(t[initial_estimation_field]) 
     160            if not latest_initial_estimate: 
     161                latest_initial_estimate = Decimal(0) 
     162             
     163            # Fetch change history for status and effort fields for this ticket 
     164            history_cursor = db.cursor() 
     165            history_cursor.execute("SELECT "  
     166                "DISTINCT c.field as field, c.time AS time, c.oldvalue as oldvalue, c.newvalue as newvalue "  
     167                "FROM ticket t, ticket_change c " 
     168                "WHERE t.id = %s and c.ticket = t.id and (c.field=%s or c.field=%s or c.field='status')" 
     169                "ORDER BY c.time ASC", [t['id'], estimation_field, initial_estimation_field]) 
     170             
     171            # Build up two dictionaries, mapping dates when effort/status 
     172            # changed, to the latest effort/status on that day (in case of 
     173            # several changes on the same day). Also record the oldest known 
     174            # effort/status, i.e. that at the time of ticket creation 
     175             
     176            estimate_history = {} 
     177            initial_estimate_history = {} 
     178            status_history = {} 
     179             
     180            earliest_estimate = None 
     181            earliest_initial_estimate = None 
     182            earliest_status = None 
     183             
     184            for row in history_cursor: 
     185                row_field, row_time, row_old, row_new = row 
     186                event_date = datetime.fromtimestamp(row_time, utc).date() 
     187                if row_field == estimation_field: 
     188                    new_value = self._cast_estimate(row_new) 
     189                    if new_value is not None: 
     190                        estimate_history[event_date] = new_value 
     191                    if earliest_estimate is None: 
     192                        earliest_estimate = self._cast_estimate(row_old) 
     193                # note: not using elif since estimation_field and initial_estimate_field could be the same! 
     194                if row_field == initial_estimation_field: 
     195                    new_value = self._cast_estimate(row_new) 
     196                    if new_value is not None: 
     197                        initial_estimate_history[event_date] = new_value 
     198                    if earliest_initial_estimate is None: 
     199                        earliest_initial_estimate = self._cast_estimate(row_old) 
     200                if row_field == 'status': 
     201                    status_history[event_date] = row_new 
     202                    if earliest_status is None: 
     203                        earliest_status = row_old 
     204             
     205            # If we don't know already (i.e. the ticket effort/status was  
     206            # not changed on the creation date), set the effort on the 
     207            # creation date. It may be that we don't have an "earliest" 
     208            # estimate/status, because it was never changed. In this case, 
     209            # use the current (latest) value. 
     210             
     211            if not creation_date in estimate_history: 
     212                if earliest_estimate is not None: 
     213                    estimate_history[creation_date] = earliest_estimate 
     214                else: 
     215                    estimate_history[creation_date] = latest_estimate 
     216             
     217            if not creation_date in initial_estimate_history: 
     218                if earliest_initial_estimate is not None: 
     219                    initial_estimate_history[creation_date] = earliest_initial_estimate 
     220                else: 
     221                    initial_estimate_history[creation_date] = latest_initial_estimate 
     222             
     223            if not creation_date in status_history: 
     224                if earliest_status is not None: 
     225                    status_history[creation_date] = earliest_status 
     226                else: 
     227                    status_history[creation_date] = latest_status 
     228             
     229            # There is a risk that the history table is messed up. Trust 
     230            # the ticket/ticket_custom tables more. 
     231             
     232            estimate_history[options['today']] = latest_estimate 
     233            initial_estimate_history[options['today']] = latest_initial_estimate 
     234            status_history[options['today']] = latest_status 
     235             
     236            # Finally add estimates to the timetable. Treat any period where the 
     237            # ticket was closed as estimate 0. We need to loop from ticket 
     238            # creation date, not just from the timetable start date, since 
     239            # it's possible that the ticket was changed between these two 
     240            # dates. 
     241 
     242            current_date = creation_date 
     243            current_estimate = None 
     244            previous_initial_estimate = Decimal(0) 
     245            is_open = None 
     246 
     247            while current_date <= options['enddate']: 
     248             
     249                if current_date in status_history: 
     250                    is_open = (status_history[current_date] not in self.closed_states) 
     251             
     252                if current_date in estimate_history: 
     253                    current_estimate = estimate_history[current_date] 
     254             
     255                if current_date in initial_estimate_history: 
     256                    effort_delta = initial_estimate_history[current_date] - previous_initial_estimate 
     257                    if effort_delta != Decimal(0): 
     258                        previous_initial_estimate = initial_estimate_history[current_date]                     
     259                        if current_date > options['startdate'] and current_date in delta: 
     260                            delta[current_date] += effort_delta 
     261 
     262                if current_date in timetable and current_date >= options['startdate'] and is_open: 
     263                    timetable[current_date] += current_estimate 
     264             
     265                current_date += timedelta(days=1) 
     266         
     267        return timetable, delta 
     268         
     269    def _scale_data(self, timetable, dates, maxhours, options): 
     270        # create sorted list of dates 
     271         
     272        if maxhours <= Decimal(0): 
     273            maxhours = Decimal(100) 
     274        ydata = [str(self._round(timetable[d] * Decimal(100) / maxhours)) 
     275                 for d in dates] 
     276        xdata = [str(self._round(x * Decimal(100) / (len(dates) - 1))) 
     277                 for x in range((options['enddate'] - options['startdate']).days + 1)] 
     278         
     279        # mark ydata invalid that is after today 
     280        if options['enddate'] > options['today']: 
     281            remaining_days = (options['enddate'] - options['today']).days; 
     282            ydata = ydata[: - remaining_days] + ['-1' for x in xrange(0, remaining_days)] 
     283         
     284        return xdata, ydata, maxhours 
     285     
     286    def _mark_downtime(self, options, dates, xdata, ydata): 
     287         
     288        # Only mark weekends if we are showning a day-by-day burndown 
     289        if options.get('interval_days', 1) != 1: 
     290            return [] 
     291             
    68292        weekends = [] 
    69293        saturday = None 
     
    84308        if len(dates) > 0 and dates[ - 1].weekday() == 5: 
    85309            weekends.append("R,f1f1f1,0,%s,1.0" % (Decimal(1) - halfday)) 
    86              
    87         title = '' 
    88         if options.get('milestone'): 
     310        return weekends 
     311     
     312    def _get_title(self, options): 
     313        title = options.get('title', '') 
     314        if not title and 'milestone' in options: 
    89315            title = options['milestone'].split('|')[0] 
    90          
    91         return Markup("<img src=\"http://chart.apis.google.com/chart?" 
    92                "chs=%sx%s"  
    93                "&amp;chd=t:%s|%s" 
    94                "&amp;cht=lxy" 
    95                "&amp;chxt=x,x,x,y" 
    96                "&amp;chxl=%s" 
    97                "&amp;chxr=%s" 
    98                "&amp;chm=%s" 
    99                "&amp;chg=100.0,100.0,1,0"  # create top and right bounding line by using grid 
    100                "&amp;chco=%s" 
    101                "&amp;chtt=%s\" " 
    102                "alt=\'Burndown Chart\' />"  
    103                % (options['width'], options['height'], 
    104                   ",".join(xdata), ",".join(ydata), bottomaxis, leftaxis, 
    105                   "|".join(weekends), options['color'], title)) 
    106                  
    107     def _calculate_timetable(self, options, query_args, req): 
    108         db = self.env.get_db_cnx() 
    109  
    110         # create dictionary with entry for each day of the required time period 
    111         timetable = {} 
    112          
    113         current_date = options['startdate'] 
    114         while current_date <= options['enddate']: 
    115             timetable[current_date] = Decimal(0) 
    116             current_date += timedelta(days=1) 
    117  
    118         # get current values for all tickets within milestone and sprints      
    119          
    120         query_args[self.estimation_field + "!"] = None 
    121         tickets = execute_query(self.env, req, query_args) 
    122  
    123         # add the open effort for each ticket for each day to the timetable 
    124  
    125         for t in tickets: 
    126              
    127             # Record the current (latest) status and estimate, and ticket 
    128             # creation date 
    129              
    130             creation_date = t['time'].date() 
    131             latest_status = t['status'] 
    132             latest_estimate = self._cast_estimate(t[self.estimation_field]) 
    133             if latest_estimate is None: 
    134                 latest_estimate = Decimal(0) 
    135              
    136             # Fetch change history for status and effort fields for this ticket 
    137             history_cursor = db.cursor() 
    138             history_cursor.execute("SELECT "  
    139                 "DISTINCT c.field as field, c.time AS time, c.oldvalue as oldvalue, c.newvalue as newvalue "  
    140                 "FROM ticket t, ticket_change c " 
    141                 "WHERE t.id = %s and c.ticket = t.id and (c.field=%s or c.field='status')" 
    142                 "ORDER BY c.time ASC", [t['id'], self.estimation_field]) 
    143              
    144             # Build up two dictionaries, mapping dates when effort/status 
    145             # changed, to the latest effort/status on that day (in case of 
    146             # several changes on the same day). Also record the oldest known 
    147             # effort/status, i.e. that at the time of ticket creation 
    148              
    149             estimate_history = {} 
    150             status_history = {} 
    151              
    152             earliest_estimate = None 
    153             earliest_status = None 
    154              
    155             for row in history_cursor: 
    156                 row_field, row_time, row_old, row_new = row 
    157                 event_date = datetime.fromtimestamp(row_time, utc).date() 
    158                 if row_field == self.estimation_field: 
    159                     new_value = self._cast_estimate(row_new) 
    160                     if new_value is not None: 
    161                         estimate_history[event_date] = new_value 
    162                     if earliest_estimate is None: 
    163                         earliest_estimate = self._cast_estimate(row_old) 
    164                 elif row_field == 'status': 
    165                     status_history[event_date] = row_new 
    166                     if earliest_status is None: 
    167                         earliest_status = row_old 
    168              
    169             # If we don't know already (i.e. the ticket effort/status was  
    170             # not changed on the creation date), set the effort on the 
    171             # creation date. It may be that we don't have an "earliest" 
    172             # estimate/status, because it was never changed. In this case, 
    173             # use the current (latest) value. 
    174              
    175             if not creation_date in estimate_history: 
    176                 if earliest_estimate is not None: 
    177                     estimate_history[creation_date] = earliest_estimate 
    178                 else: 
    179                     estimate_history[creation_date] = latest_estimate 
    180             if not creation_date in status_history: 
    181                 if earliest_status is not None: 
    182                     status_history[creation_date] = earliest_status 
    183                 else: 
    184                     status_history[creation_date] = latest_status 
    185              
    186             # Finally estimates to the timetable. Treat any period where the 
    187             # ticket was closed as estimate 0. We need to loop from ticket 
    188             # creation date, not just from the timetable start date, since 
    189             # it's possible that the ticket was changed between these two 
    190             # dates. 
    191  
    192             current_date = creation_date 
    193             current_estimate = None 
    194             is_open = None 
    195  
    196             while current_date <= options['enddate']: 
    197                 if current_date in status_history: 
    198                     is_open = (status_history[current_date] not in self.closed_states) 
    199                  
    200                 if current_date in estimate_history: 
    201                     current_estimate = estimate_history[current_date] 
    202  
    203                 if current_date >= options['startdate'] and is_open: 
    204                     timetable[current_date] += current_estimate 
    205  
    206                 current_date += timedelta(days=1) 
    207   
    208         return timetable 
    209          
    210     def _scale_data(self, timetable, options): 
    211         # create sorted list of dates 
    212         dates = timetable.keys() 
    213         dates.sort() 
    214  
    215         maxhours = max(timetable.values()) 
    216                  
    217         if maxhours <= Decimal(0): 
    218             maxhours = Decimal(100) 
    219         ydata = [str(self._round(timetable[d] * Decimal(100) / maxhours)) 
    220                  for d in dates] 
    221         xdata = [str(self._round(x * Decimal(100) / (len(dates) - 1))) 
    222                  for x in range((options['enddate'] - options['startdate']).days + 1)] 
    223          
    224         # mark ydata invalid that is after today 
    225         if options['enddate'] > options['today']: 
    226             remaining_days = (options['enddate'] - options['today']).days; 
    227             ydata = ydata[: - remaining_days] + ['-1' for x in xrange(0, remaining_days)] 
    228          
    229         return xdata, ydata, maxhours 
     316        return title 
    230317     
    231318    def _round(self, decimal_): 
  • estimationtoolsplugin/branches/optilude-change-line/estimationtools/tests/burndownchart.py

    r4733 r4909  
    1515        self.env = EnvironmentStub(default_data = True) 
    1616        self.env.config.set('ticket-custom', 'hours_remaining', 'text') 
     17        self.env.config.set('ticket-custom', 'hours_initial', 'text') 
    1718        self.env.config.set('estimation-tools', 'estimation_field', 'hours_remaining') 
     19        self.env.config.set('estimation-tools', 'initial_estimation_field', 'hours_initial') 
    1820        self.req = Mock(href = Href('/'), 
    1921                        abs_href = Href('http://www.example.com/'), 
     
    2527        ticket['summary'] = 'Test Ticket' 
    2628        ticket['hours_remaining'] = estimation 
     29        ticket['hours_initial'] = estimation 
    2730        ticket['milestone'] = 'milestone1' 
    2831        return ticket.insert() 
     
    3437        for key in keys: 
    3538            ticket['hours_remaining'] = history[key] 
     39            ticket.save_changes("me", "testing", datetime.combine(key, datetime.now(utc).timetz())) 
     40     
     41    def _change_ticket_initial_estimations(self, id, history): 
     42        ticket = Ticket(self.env, id) 
     43        keys = history.keys() 
     44        keys.sort() 
     45        for key in keys: 
     46            ticket['hours_initial'] = history[key] 
    3647            ticket.save_changes("me", "testing", datetime.combine(key, datetime.now(utc).timetz())) 
    3748             
     
    5566        db = self.env.get_db_cnx() 
    5667        options, query_args = parse_options(db, "milestone=milestone1, startdate=2008-02-20, enddate=2008-02-28", {}) 
    57         timetable = chart._calculate_timetable(options, query_args, self.req) 
    58         xdata, ydata, maxhours = chart._scale_data(timetable, options) 
     68        timetable, _ = chart._calculate_timetable(options, query_args, self.req) 
     69        dates = sorted(timetable.keys()) 
     70        xdata, ydata, maxhours = chart._scale_data(timetable, dates, Decimal(0), options) 
    5971        self.assertEqual(xdata, ['0.00', '12.50', '25.00', '37.50', '50.00', '62.50', '75.00', '87.50', '100.00']) 
    6072        self.assertEqual(ydata, ['0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00']) 
     
    7183        day2 = day1 + timedelta(days=1) 
    7284        day3 = day2 + timedelta(days=1) 
    73         options = {'today': day3, 'startdate': day1, 'enddate': day3, 'closedstates': ['closed']
     85        options = {'today': day3, 'startdate': day1, 'enddate': day3
    7486        query_args = {'milestone': "milestone1"} 
    7587        self._insert_ticket('10') 
    76         timetable = chart._calculate_timetable(options, query_args, self.req) 
     88        timetable, _ = chart._calculate_timetable(options, query_args, self.req) 
    7789        self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(10), day3: Decimal(10)}) 
    7890         
     
    8294        day2 = day1 + timedelta(days=1) 
    8395        day3 = day2 + timedelta(days=1) 
    84         options = {'today': day3, 'startdate': day1, 'enddate': day3, 'closedstates': ['closed']
     96        options = {'today': day3, 'startdate': day1, 'enddate': day3
    8597        self._insert_ticket('10') 
    86         timetable = chart._calculate_timetable(options, {}, self.req) 
     98        timetable, _ = chart._calculate_timetable(options, {}, self.req) 
    8799        self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(10), day3: Decimal(10)}) 
    88100         
     
    92104        day2 = day1 + timedelta(days=1) 
    93105        day3 = day2 + timedelta(days=1) 
    94         options = {'today': day3, 'startdate': day1, 'enddate': day3, 'closedstates': ['closed']
     106        options = {'today': day3, 'startdate': day1, 'enddate': day3
    95107        query_args = {'milestone': "milestone1"} 
    96108        ticket1 = self._insert_ticket('10') 
    97109        self._change_ticket_estimations(ticket1, {day2:'5', day3:'0'}) 
    98110      
    99         timetable = chart._calculate_timetable(options, query_args, self.req) 
     111        timetable, _ = chart._calculate_timetable(options, query_args, self.req) 
    100112        self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(5), day3: Decimal(0)}) 
    101113         
     
    105117        day2 = day1 + timedelta(days=1) 
    106118        day3 = day2 + timedelta(days=1) 
    107         options = {'today': day3, 'startdate': day1, 'enddate': day3, 'closedstates': ['closed']
     119        options = {'today': day3, 'startdate': day1, 'enddate': day3
    108120        query_args = {'milestone': "milestone1"} 
    109121        ticket1 = self._insert_ticket('10') 
    110122        self._change_ticket_estimations(ticket1, {day2:'5'}) 
    111123        self._change_ticket_states(ticket1, {day3: 'closed'}) 
    112         timetable = chart._calculate_timetable(options, query_args, self.req) 
     124        timetable, _ = chart._calculate_timetable(options, query_args, self.req) 
    113125        self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(5), day3: Decimal(0)}) 
    114126 
     
    118130        day2 = day1 + timedelta(days=1) 
    119131        day3 = day2 + timedelta(days=1) 
    120         options = {'today': day3, 'startdate': day1, 'enddate': day3, 'closedstates': ['closed']
     132        options = {'today': day3, 'startdate': day1, 'enddate': day3
    121133        query_args = {'milestone': "milestone1"} 
    122134        ticket1 = self._insert_ticket('10') 
    123135        self._change_ticket_states(ticket1, {day2: 'closed'}) 
    124136        self._change_ticket_estimations(ticket1, {day3:'5'}) 
    125         timetable = chart._calculate_timetable(options, query_args, self.req) 
     137        timetable, _ = chart._calculate_timetable(options, query_args, self.req) 
    126138        self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(0), day3: Decimal(0)}) 
    127139 
     
    132144        day3 = day2 + timedelta(days=1) 
    133145        day4 = day3 + timedelta(days=1) 
    134         options = {'today': day4, 'startdate': day1, 'enddate': day4, 'closedstates': ['closed']
     146        options = {'today': day4, 'startdate': day1, 'enddate': day4
    135147        query_args = {'milestone': "milestone1"} 
    136148        ticket1 = self._insert_ticket('10') 
    137149        self._change_ticket_estimations(ticket1, {day3:'5'}) 
    138150        self._change_ticket_states(ticket1, {day2: 'closed', day4: 'new'}) 
    139         timetable = chart._calculate_timetable(options, query_args, self.req) 
     151        timetable, _ = chart._calculate_timetable(options, query_args, self.req) 
    140152        self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(0), day3: Decimal(0), day4: Decimal(5)}) 
    141153         
     
    145157        day2 = day1 + timedelta(days=1) 
    146158        day3 = day2 + timedelta(days=1) 
    147         options = {'today': day3, 'startdate': day1, 'enddate': day3, 'closedstates': ['closed']
     159        options = {'today': day3, 'startdate': day1, 'enddate': day3
    148160        query_args = {'milestone': "milestone1"} 
    149161        ticket1 = self._insert_ticket('10') 
     
    152164        self._change_ticket_estimations(ticket2, {day2:'1', day3:'2'}) 
    153165      
    154         timetable = chart._calculate_timetable(options, query_args, self.req) 
     166        timetable, _ = chart._calculate_timetable(options, query_args, self.req) 
    155167        self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(6), day3: Decimal(2)}) 
    156168 
     
    161173        day3 = day2 + timedelta(days=1) 
    162174        day4 = day3 + timedelta(days=1) 
    163         options = {'today': day3, 'startdate': day1, 'enddate': day3, 'closedstates': ['closed']
     175        options = {'today': day4, 'startdate': day1, 'enddate': day3
    164176        query_args = {'milestone': "milestone1"} 
    165177        ticket1 = self._insert_ticket('10') 
    166178        self._change_ticket_estimations(ticket1, {day2:'5', day4:''}) 
    167179      
    168         timetable = chart._calculate_timetable(options, query_args, self.req) 
     180        timetable, _ = chart._calculate_timetable(options, query_args, self.req) 
    169181        self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(5), day3: Decimal(5)}) 
    170182     
     
    174186        day2 = day1 + timedelta(days=1) 
    175187        day3 = day2 + timedelta(days=1) 
    176         options = {'today': day3, 'startdate': day1, 'enddate': day3, 'closedstates': ['closed']
     188        options = {'today': day3, 'startdate': day1, 'enddate': day3
    177189        query_args = {'milestone': "milestone1"} 
    178190        ticket1 = self._insert_ticket('10') 
    179191        self._change_ticket_estimations(ticket1, {day2: 'IGNOREME', day3:'5'}) 
    180         timetable = chart._calculate_timetable(options, query_args, self.req) 
     192        timetable, _ = chart._calculate_timetable(options, query_args, self.req) 
    181193        self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(10), day3: Decimal(5)}) 
     194         
     195    def test_calculate_delta_no_new_or_changed(self): 
     196        chart = BurndownChart(self.env) 
     197        day1 = datetime.now(utc).date() 
     198        day2 = day1 + timedelta(days=1) 
     199        day3 = day2 + timedelta(days=1) 
     200        options = {'today': day3, 'startdate': day1, 'enddate': day3, 'change': True} 
     201        query_args = {'milestone': "milestone1"} 
     202        ticket1 = self._insert_ticket('10') 
     203        self._change_ticket_estimations(ticket1, {day2:'5', day3:''}) 
     204        ticket2 = self._insert_ticket('0') 
     205        self._change_ticket_estimations(ticket2, {day2:'1', day3:'2'}) 
     206      
     207        timetable, delta = chart._calculate_timetable(options, query_args, self.req) 
     208        self.assertEqual(timetable, {day1: Decimal(10), day2: Decimal(6), day3: Decimal(2)}) 
     209        self.assertEqual(delta, {day1: Decimal(0), day2: Decimal(0), day3: Decimal(0)}) 
     210                 
     211    def test_calculate_delta_with_changed_estimates(self): 
     212        chart = BurndownChart(self.env) 
     213        day1 = datetime.now(utc).date() 
     214        day2 = day1 + timedelta(days=1) 
     215        day3 = day2 + timedelta(days=1) 
     216        options = {'today': day3, 'startdate': day1, 'enddate': day3, 'change': True} 
     217        query_args = {'milestone': "milestone1"} 
     218        ticket1 = self._insert_ticket('10') 
     219        ticket2 = self._insert_ticket('5') 
     220        self._change_ticket_initial_estimations(ticket1, {day2:'8'}) # -2 
     221        self._change_ticket_initial_estimations(ticket2, {day3:'8'}) # +3 
     222        import time; time.sleep(1) # Avoid time resolution problem 
     223        self._change_ticket_estimations(ticket2, {day2:'1', day3:'2'}) 
     224      
     225        timetable, delta = chart._calculate_timetable(options, query_args, self.req) 
     226        self.assertEqual(timetable, {day1: Decimal(15), day2: Decimal(11), day3: Decimal(12)}) 
     227        self.assertEqual(delta, {day1: Decimal(0), day2: Decimal(-2), day3: Decimal(3)}) 
     228         
     229    def test_date_intervals(self): 
     230        chart = BurndownChart(self.env) 
     231        day1 = datetime.now(utc).date() 
     232        day2 = day1 + timedelta(days=1) 
     233        day3 = day2 + timedelta(days=1) 
     234        day4 = day3 + timedelta(days=1) 
     235        day5 = day4 + timedelta(days=1) 
     236        options = {'today': day5, 'startdate': day1, 'enddate': day5, 'interval_days': 2} 
     237        query_args = {'milestone': "milestone1"} 
     238        ticket1 = self._insert_ticket('10') 
     239        ticket2 = self._insert_ticket('5') 
     240        self._change_ticket_estimations(ticket2, {day3:'1', day4:'2', day5:'3'}) 
     241 
     242        timetable, delta = chart._calculate_timetable(options, query_args, self.req) 
     243         
     244        self.assertEqual(timetable, {day1: Decimal(15), day3: Decimal(11), day5: Decimal(13)}) 
     245        self.assertEqual(delta, {day1: Decimal(0), day3: Decimal(0), day5: Decimal(0)}) 
     246     
     247    def test_date_intervals_always_includes_today(self): 
     248        chart = BurndownChart(self.env) 
     249        day1 = datetime.now(utc).date() 
     250        day2 = day1 + timedelta(days=1) 
     251        day3 = day2 + timedelta(days=1) 
     252        day4 = day3 + timedelta(days=1) 
     253        day5 = day4 + timedelta(days=1) 
     254        day6 = day4 + timedelta(days=1) 
     255        options = {'today': day5, 'startdate': day1, 'enddate': day5, 'interval_days': 2} 
     256        query_args = {'milestone': "milestone1"} 
     257        ticket1 = self._insert_ticket('10') 
     258        ticket2 = self._insert_ticket('5') 
     259        self._change_ticket_estimations(ticket2, {day3:'1', day4:'2', day5:'3',day6:'4'}) 
     260 
     261        timetable, delta = chart._calculate_timetable(options, query_args, self.req) 
     262         
     263        self.assertEqual(timetable, {day1: Decimal(15), day3: Decimal(11), day5: Decimal(13), day6: Decimal(14)}) 
     264        self.assertEqual(delta, {day1: Decimal(0), day3: Decimal(0), day5: Decimal(0), day6: Decimal(0)}) 
  • estimationtoolsplugin/branches/optilude-change-line/estimationtools/tests/workloadchart.py

    r4448 r4909  
    3232        result = workload_chart.render_macro(self.req, "", "milestone=milestone1") 
    3333        self.assertEqual(result, u'<img src="http://chart.apis.google.com/chart?chs=400x100&amp;'\ 
    34                          'chd=t:10,30,20&amp;cht=p3&amp;chtt=Workload 60h (0 workdays left)&amp;'\ 
     34                         'chd=t:10,30,20&amp;cht=p3&amp;chtt=Workload 60h (1 workdays left)&amp;'\ 
    3535                         'chl=A 10h|C 30h|B 20h&amp;chco=ff9900" alt=\'Workload Chart\' />') 
    3636 
     
    4444        result = workload_chart.render_macro(self.req, "", "milestone=milestone1") 
    4545        self.assertEqual(result, u'<img src="http://chart.apis.google.com/chart?chs=400x100&amp;'\ 
    46                          'chd=t:10,30,20&amp;cht=p3&amp;chtt=Workload 60h (0 workdays left)&amp;'\ 
     46                         'chd=t:10,30,20&amp;cht=p3&amp;chtt=Workload 60h (1 workdays left)&amp;'\ 
    4747                         'chl=A 10h|C 30h|B 20h&amp;chco=ff9900" alt=\'Workload Chart\' />' ) 
  • estimationtoolsplugin/branches/optilude-change-line/estimationtools/utils.py

    r4893 r4909  
    66from trac.ticket.query import Query 
    77 
    8 AVAILABLE_OPTIONS = ['startdate', 'enddate', 'today', 'width', 'height', 'color'
     8AVAILABLE_OPTIONS = ['startdate', 'enddate', 'today', 'width', 'height', 'color', 'title', 'change', 'interval_days'
    99 
    1010def get_estimation_field():     
     
    1212        doc="""Defines what custom field should be used to calculate estimation charts. 
    1313        Defaults to 'estimatedhours'""") 
     14 
     15def get_initial_estimation_field():     
     16    return Option('estimation-tools', 'initial_estimation_field', '',  
     17        doc="""When calculating project change, use this field instead of the one set for estimation_field. 
     18        Defaults to be the same as 'estimation_field'""") 
    1419 
    1520def get_closed_states(): 
     
    6267        options['today'] = datetime.now().date() 
    6368     
     69    if 'interval_days' in options: 
     70        try: 
     71            options['interval_days'] = int(options['interval_days']) 
     72        except (ValueError, TypeError): 
     73            options['interval_days'] = 1 
     74    else: 
     75        options['interval_days'] = 1 
     76         
     77    if 'change' in options and options['change'].lower() not in ('false', '0'): 
     78        options['change'] = True 
     79    else: 
     80        options['change'] = False 
     81     
    6482    # all arguments that are no key should be treated as part of the query   
    6583    query_args = {} 
     
    7088 
    7189def execute_query(env, req, query_args): 
    72     # set maximum number of returned tickets to 0 to get all tickets at once 
    73     query_args['max'] = 0 
    7490    query_string = '&'.join(['%s=%s' % item for item in query_args.iteritems()]) 
    7591    query = Query.from_string(env, query_string)