Changeset 4448

Show
Ignore:
Timestamp:
10/12/08 16:33:02 (3 months ago)
Author:
hoessler
Message:

Implemented flexible selection of tickets using the mini query language.
Closes #3814

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • estimationtoolsplugin/trunk/estimationtools/burndownchart.py

    r4332 r4448  
    55from trac.wiki.macros import WikiMacroBase 
    66import copy 
    7 import re 
    87 
    98DEFAULT_OPTIONS = {'width': '800', 'height': '200', 'color': 'ff9900'} 
    109 
    1110class BurndownChart(WikiMacroBase): 
    12     """Creates burn down chart for given milestone
     11    """Creates burn down chart for selected tickets
    1312 
    14     This macro creates a chart that can be used to visualize the progress in a milestone (aka sprint or  
     13    This macro creates a chart that can be used to visualize the progress in a milestone (e.g., sprint or  
    1514    product backlog).  
    16     For a given milestone and time frame, the remaining, estimated effort is calculated. 
     15    For a given set of tickets and a time frame, the remaining estimated effort is calculated. 
    1716     
    1817    The macro has the following parameters: 
    19      * `milestone`: '''mandatory''' parameter that specifies the milestone. 
     18     * a comma-separated list of query parameters for the ticket selection, in the form "key=value" as specified in TracQuery#QueryLanguage. 
    2019     * `startdate`: '''mandatory''' parameter that specifies the start date of the period (ISO8601 format) 
    2120     * `enddate`: end date of the period. If omitted, it defaults to either the milestones `completed' date,  
    22        or `due`date, or today (in that order) (ISO8601 format) 
    23      * `sprints`: list of comma-separated name of sprints to be included in calculation. Must be surrounded by 
    24        brackets. 
     21       or `due` date, or today (in that order) (ISO8601 format) 
    2522     * `width`: width of resulting diagram (defaults to 800) 
    2623     * `height`: height of resulting diagram (defaults to 200) 
     
    3027    Examples: 
    3128    {{{ 
    32         [[BurndownChart(milestone = Sprint 1, startdate = 2008-01-01)]] 
    33         [[BurndownChart(milestone = Release 3.0, startdate = 2008-01-01, enddate = 2008-01-15, 
    34             width = 600, height = 100, color = 0000ff, sprints = (Sprint 1, Sprint 2))]] 
     29        [[BurndownChart(milestone=Sprint 1, startdate=2008-01-01)]] 
     30        [[BurndownChart(milestone=Release 3.0|Sprint 1, startdate=2008-01-01, enddate=2008-01-15, 
     31            width=600, height=100, color=0000ff)]] 
    3532    }}} 
    3633    """ 
     
    3936     
    4037    def render_macro(self, req, name, content): 
    41         # you need 'TICKT_VIEW' or 'TICKET_VIEW_CC' (see PrivateTicketPatch) permissions 
    42         if not (req.perm.has_permission('TICKET_VIEW') or  
    43                 req.perm.has_permission('TICKET_VIEW_CC')): 
    44             raise TracError('TICKET_VIEW or TICKET_VIEW_CC permission required') 
    45         options = copy.copy(DEFAULT_OPTIONS) 
    46          
    47         # replace all ',' in brackets with ';' to avoid splitting list of sprints 
    48         def repl(match): 
    49             return match.group().replace(',', ';') 
    50         regexp = re.compile(r'\((.*)\)') 
    51         content = regexp.sub(repl, content) 
    52          
    53         if content: 
    54             for arg in content.split(','): 
    55                 i = arg.index('=') 
    56                 options[arg[:i].strip()] = arg[i + 1:].strip() 
     38        # prepare options 
     39        options, query_args = parse_options(self.env.get_db_cnx(), content, copy.copy(DEFAULT_OPTIONS)) 
    5740 
    58         # prepare options 
    59         options = parse_options(self.env.get_db_cnx(), options) 
    6041        if not options['startdate']: 
    6142            raise TracError("No start date specified!") 
    62          
    63         # parse list of sprints 
    64         sprintsarg = options.get('sprints') 
    65         if sprintsarg: 
    66             options['sprints'] = sprintsarg.strip('()').split(';') 
    67          
     43                
    6844        # calculate data 
    69         timetable = self._calculate_timetable(options
     45        timetable = self._calculate_timetable(options, query_args, req
    7046         
    7147        # scale data       
     
    11692                  "|".join(weekends), options['color'], options['milestone'].strip('\'\"'))) 
    11793                 
    118     def _calculate_timetable(self, options): 
     94    def _calculate_timetable(self, options, query_args, req): 
    11995        db = self.env.get_db_cnx() 
    120         cursor = db.cursor() 
    12196 
    12297        # create dictionary with entry for each day of the required time period 
     
    129104 
    130105        # get current values for all tickets within milestone and sprints      
    131         sprints = options.get('sprints') 
    132         if not sprints: 
    133             sprints = [] 
    134              
    135         sprints = [options['milestone']] + sprints 
     106         
     107        query_args[self.estimation_field + "!"] = None 
     108        tickets = execute_query(self.env, req, query_args) 
    136109 
    137         select_tickets = ("SELECT " 
    138            "id, time, p.value as estimation " 
    139            "FROM ticket t, ticket_custom p " 
    140            "WHERE p.ticket = t.id and p.name = %%s and (t.milestone in (%s)) " 
    141            "ORDER BY t.id" % (',').join(['%s' for sprint in sprints])) 
    142              
    143         cursor.execute(select_tickets, [self.estimation_field] + sprints) 
    144          
    145         for id, time, estimation in cursor: 
    146             creationdate = datetime.fromtimestamp(time).date() 
     110        # print tickets 
     111 
     112        for t in tickets: 
     113            creationdate = t['time'].date() 
     114            estimation = t[self.estimation_field] 
    147115             
    148116            # get change history for each ticket 
     
    152120                "FROM ticket t, ticket_change c " 
    153121                "WHERE t.id = %s and c.ticket = t.id and c.field=%s " 
    154                 "ORDER BY c.time DESC", [id, self.estimation_field]) 
     122                "ORDER BY c.time DESC", [t['id'], self.estimation_field]) 
    155123             
    156124            nextchangedate = None 
     
    158126            row = history_cursor.fetchone() 
    159127            if row: 
    160                 nextchangedate = datetime.fromtimestamp(row[0]).date() 
     128                nextchangedate = datetime.fromtimestamp(row[0], utc).date() 
    161129                nextvalue = row[1] 
    162130 
     
    169137                    row = history_cursor.fetchone() 
    170138                    if row: 
    171                         nextchangedate = datetime.fromtimestamp(row[0]).date() 
     139                        nextchangedate = datetime.fromtimestamp(row[0], utc).date() 
    172140                        nextvalue = row[1] 
    173141                    else: 
  • estimationtoolsplugin/trunk/estimationtools/hoursremaining.py

    r4332 r4448  
    1 from estimationtools.utils import get_estimation_field 
    2 from trac.core import TracError 
     1from estimationtools.utils import get_estimation_field, execute_query 
    32from trac.wiki.macros import WikiMacroBase 
     3from trac.wiki.api import parse_args 
    44 
    55class HoursRemaining(WikiMacroBase): 
    6     """Calculates remaining estimated hours for given milestone
     6    """Calculates remaining estimated hours for the queried tickets
    77 
    8     `milestone` is a mandatory parameter. 
     8    The macro accepts a comma-separated list of query parameters for the ticket selection,  
     9    in the form "key=value" as specified in TracQuery#QueryLanguage. 
    910     
    1011    Example: 
     
    1718     
    1819    def render_macro(self, req, name, content): 
    19         # you need 'TICKT_VIEW' or 'TICKET_VIEW_CC' (see PrivateTicketPatch) permissions 
    20         if not (req.perm.has_permission('TICKET_VIEW') or  
    21                 req.perm.has_permission('TICKET_VIEW_CC')): 
    22             raise TracError('TICKET_VIEW or TICKET_VIEW_CC permission required') 
    23         options = {} 
    24         if content: 
    25             for arg in content.split(','): 
    26                 i = arg.index('=') 
    27                 options[arg[:i].strip()] = arg[i+1:].strip() 
    28         milestone = options.get('milestone') 
    29         if not milestone: 
    30             raise TracError("No milestone specified!") 
    31         db = self.env.get_db_cnx() 
    32         cursor = db.cursor() 
    33         cursor.execute("SELECT p.value as estimation" 
    34                        "  FROM ticket t, ticket_custom p" 
    35                        "  WHERE p.ticket = t.id and p.name = %s" 
    36                        "  AND t.milestone = %s", [self.estimation_field, milestone]) 
     20        _, options = parse_args(content, strict=False) 
     21 
     22        # we have to add custom estimation field to query so that field is added to 
     23        # resulting ticket list 
     24        options[self.estimation_field + "!"] = None 
     25 
     26        tickets = execute_query(self.env, req, options) 
    3727         
    3828        sum = 0.0 
    39         for estimation, in cursor
     29        for t in tickets
    4030            try: 
    41                 sum += float(estimation
     31                sum += float(t[self.estimation_field]
    4232            except: 
    4333                pass 
  • estimationtoolsplugin/trunk/estimationtools/tests/burndownchart.py

    r4361 r4448  
    11from estimationtools.burndownchart import * 
    22from estimationtools.utils import * 
    3 from trac.test import EnvironmentStub 
     3from trac.test import EnvironmentStub, MockPerm, Mock 
    44from trac.ticket.model import Ticket 
    55from trac.util.datefmt import utc 
     6from trac.web.href import Href 
    67import time 
    78import unittest 
     
    1415        self.env.config.set('ticket-custom', 'hours_remaining', 'text') 
    1516        self.env.config.set('estimation-tools', 'estimation_field', 'hours_remaining') 
     17        self.req = Mock(href = Href('/'), 
     18                        abs_href = Href('http://www.example.com/'), 
     19                        perm = MockPerm(), 
     20                        authname='anonymous') 
    1621         
    1722    def _insert_ticket(self, estimation): 
     
    3237    def test_parse_options(self): 
    3338        db = self.env.get_db_cnx() 
    34         options = parse_options(db, {"milestone":"milestone1", "startdate":"2008-02-20", 
    35                              "enddate":"2008-02-28"}) 
    36         self.assertNotEqual(options['milestone'], None) 
     39        options, query_args = parse_options(db, "milestone=milestone1, startdate=2008-02-20, enddate=2008-02-28", {}) 
     40        self.assertNotEqual(query_args['milestone'], None) 
    3741        self.assertNotEqual(options['startdate'], None) 
    3842        self.assertNotEqual(options['enddate'], None) 
     
    4145        chart = BurndownChart(self.env) 
    4246        db = self.env.get_db_cnx() 
    43         options = parse_options(db, {"milestone":"milestone1", "startdate":"2008-02-20", 
    44                              "enddate":"2008-02-28"}) 
    45         timetable = chart._calculate_timetable(options) 
     47        options, query_args = parse_options(db, "milestone=milestone1, startdate=2008-02-20, enddate=2008-02-28", {}) 
     48        timetable = chart._calculate_timetable(options, query_args, self.req) 
    4649        xdata, ydata, maxhours = chart._scale_data(timetable, options) 
    4750        self.assertEqual(xdata, ['0.0', '12.5', '25.0', '37.5', '50.0', '62.5', '75.0', '87.5', '100.0']) 
     
    5457        day2 = day1 + timedelta(days=1) 
    5558        day3 = day2 + timedelta(days=1) 
    56         options = {'today': day3, 'milestone': "milestone1", 'startdate': day1, 'enddate': day3} 
     59        options = {'today': day3, 'startdate': day1, 'enddate': day3} 
     60        query_args = {'milestone': "milestone1"} 
    5761        self._insert_ticket('10') 
    58         timetable = chart._calculate_timetable(options) 
     62        timetable = chart._calculate_timetable(options, query_args, self.req) 
     63        self.assertEqual(timetable, {day1: 10.0, day2: 10.0, day3: 10.0}) 
     64         
     65    def test_calculate_timetable_without_milestone(self): 
     66        chart = BurndownChart(self.env) 
     67        day1 = datetime.now(utc).date() 
     68        day2 = day1 + timedelta(days=1) 
     69        day3 = day2 + timedelta(days=1) 
     70        options = {'today': day3, 'startdate': day1, 'enddate': day3} 
     71        self._insert_ticket('10') 
     72        timetable = chart._calculate_timetable(options, {}, self.req) 
    5973        self.assertEqual(timetable, {day1: 10.0, day2: 10.0, day3: 10.0}) 
    6074         
     
    6478        day2 = day1 + timedelta(days=1) 
    6579        day3 = day2 + timedelta(days=1) 
    66         options = {'today': day3, 'milestone': "milestone1", 'startdate': day1, 'enddate': day3} 
     80        options = {'today': day3, 'startdate': day1, 'enddate': day3} 
     81        query_args = {'milestone': "milestone1"} 
    6782        ticket1 = self._insert_ticket('10') 
    6883        self._change_ticket(ticket1, {day2:'5', day3:'0'}) 
    6984      
    70         timetable = chart._calculate_timetable(options
     85        timetable = chart._calculate_timetable(options, query_args, self.req
    7186        self.assertEqual(timetable, {day1: 10.0, day2: 5.0, day3: 0.0}) 
    7287         
     
    7691        day2 = day1 + timedelta(days=1) 
    7792        day3 = day2 + timedelta(days=1) 
    78         options = {'today': day3, 'milestone': "milestone1", 'startdate': day1, 'enddate': day3} 
     93        options = {'today': day3, 'startdate': day1, 'enddate': day3} 
     94        query_args = {'milestone': "milestone1"} 
    7995        ticket1 = self._insert_ticket('10') 
    80         self._change_ticket(ticket1, {day2:'5', day3:'0'}) 
     96        self._change_ticket(ticket1, {day2:'5', day3:''}) 
    8197        ticket2 = self._insert_ticket('0') 
    8298        self._change_ticket(ticket2, {day2:'1', day3:'2'}) 
    8399      
    84         timetable = chart._calculate_timetable(options
     100        timetable = chart._calculate_timetable(options, query_args, self.req
    85101        self.assertEqual(timetable, {day1: 10.0, day2: 6.0, day3: 2.0}) 
    86102 
     
    91107        day3 = day2 + timedelta(days=1) 
    92108        day4 = day3 + timedelta(days=1) 
    93         options = {'today': day3, 'milestone': "milestone1", 'startdate': day1, 'enddate': day3} 
     109        options = {'today': day3, 'startdate': day1, 'enddate': day3} 
     110        query_args = {'milestone': "milestone1"} 
    94111        ticket1 = self._insert_ticket('10') 
    95         self._change_ticket(ticket1, {day2:'5', day4:'0'}) 
     112        self._change_ticket(ticket1, {day2:'5', day4:''}) 
    96113      
    97         timetable = chart._calculate_timetable(options
     114        timetable = chart._calculate_timetable(options, query_args, self.req
    98115        self.assertEqual(timetable, {day1: 10.0, day2: 5.0, day3: 5.0}) 
  • estimationtoolsplugin/trunk/estimationtools/tests/hoursremaining.py

    r4361 r4448  
    11from estimationtools.hoursremaining import HoursRemaining 
    2 from trac.test import EnvironmentStub, Mock 
     2from trac.test import EnvironmentStub, Mock, MockPerm 
    33from trac.ticket.model import Ticket 
    44from trac.web.href import Href 
     
    1414        self.req = Mock(href = Href('/'), 
    1515                        abs_href = Href('http://www.example.com/'), 
    16                         perm = Mock(has_permission=lambda x: x == 'TICKET_VIEW')) 
     16                        perm = MockPerm(), 
     17                        authname='anonymous') 
    1718        
    1819    def _insert_ticket(self, estimation): 
  • estimationtoolsplugin/trunk/estimationtools/tests/workloadchart.py

    r4361 r4448  
    11from estimationtools.workloadchart import WorkloadChart 
    2 from trac.test import EnvironmentStub, Mock 
     2from trac.test import EnvironmentStub, Mock, MockPerm 
    33from trac.ticket.model import Ticket 
    44from trac.web.href import Href 
     
    1414        self.req = Mock(href = Href('/'), 
    1515                        abs_href = Href('http://www.example.com/'), 
    16                         perm = Mock(has_permission=lambda x: x == 'TICKET_VIEW')) 
     16                        perm = MockPerm(), 
     17                        authname='anonymous') 
    1718        
    1819    def _insert_ticket(self, estimation, owner): 
     
    3132        result = workload_chart.render_macro(self.req, "", "milestone=milestone1") 
    3233        self.assertEqual(result, u'<img src="http://chart.apis.google.com/chart?chs=400x100&amp;'\ 
    33                          'chd=t:10,30,20&amp;cht=p3&amp;chtt=Workload 60h (1 workdays left)&amp;'\ 
     34                         'chd=t:10,30,20&amp;cht=p3&amp;chtt=Workload 60h (0 workdays left)&amp;'\ 
    3435                         'chl=A 10h|C 30h|B 20h&amp;chco=ff9900" alt=\'Workload Chart\' />') 
    3536 
     
    4344        result = workload_chart.render_macro(self.req, "", "milestone=milestone1") 
    4445        self.assertEqual(result, u'<img src="http://chart.apis.google.com/chart?chs=400x100&amp;'\ 
    45                          'chd=t:10,30,20&amp;cht=p3&amp;chtt=Workload 60h (1 workdays left)&amp;'\ 
     46                         'chd=t:10,30,20&amp;cht=p3&amp;chtt=Workload 60h (0 workdays left)&amp;'\ 
    4647                         'chl=A 10h|C 30h|B 20h&amp;chco=ff9900" alt=\'Workload Chart\' />' ) 
  • estimationtoolsplugin/trunk/estimationtools/utils.py

    r4332 r4448  
    33from trac.config import Option 
    44from trac.core import TracError 
     5from trac.wiki.api import parse_args 
     6from trac.ticket.query import Query 
     7from trac.util.datefmt import utc 
     8 
     9AVAILABLE_OPTIONS = ['startdate', 'enddate', 'today', 'width', 'height', 'color'] 
    510 
    611def get_estimation_field():     
     
    914        Defaults to 'estimatedhours'""") 
    1015 
    11 def parse_options(db, options):        
    12     """Parses the parameters, makes some sanity checks, and creates defaults values 
     16def parse_options(db, content, options):        
     17    """Parses the parameters, makes some sanity checks, and creates default values 
    1318    for missing parameters.     
    1419    """ 
     
    1722    # check arguments 
    1823    options['milestone'] = options.get('milestone') 
    19     if not options['milestone']: 
    20         raise TracError("No milestone specified!") 
     24#    if not options['milestone']: 
     25#        raise TracError("No milestone specified!") 
     26 
     27    _, parsed_options = parse_args(content, strict=False) 
     28     
     29    options.update(parsed_options) 
    2130 
    2231    startdatearg = options.get('startdate') 
     
    2837    if enddatearg: 
    2938        options['enddate'] = datetime(*strptime(enddatearg, "%Y-%m-%d")[0:5]).date() 
    30     if not options['enddate']:             
     39    options['milestone'] = options.get('milestone') 
     40 
     41    if not options['enddate'] and options['milestone']:    
     42        # use first milestone 
     43        milestone = options['milestone'].split('|')[0]          
    3144        # try to get end date from db 
    32         cursor.execute("SELECT completed, due FROM milestone WHERE name = %s", [options['milestone']]) 
     45        cursor.execute("SELECT completed, due FROM milestone WHERE name = %s", [milestone]) 
    3346        row = cursor.fetchone() 
    3447        if not row: 
    35             raise TracError("Couldn't find milestone %s" % (options['milestone'])) 
     48            raise TracError("Couldn't find milestone %s" % (milestone)) 
    3649        if row[0]: 
    3750            options['enddate'] = datetime.fromtimestamp(row[0]).date() 
    3851        elif row[1]: 
    3952            options['enddate'] = datetime.fromtimestamp(row[1]).date() 
    40         else: 
     53 
     54    if not options['enddate']: 
    4155            options['enddate'] = datetime.now().date() 
    4256    todayarg = options.get('today') 
    4357    if not todayarg: 
    4458        options['today'] = datetime.now().date() 
    45     return options 
     59       
     60    # all arguments that are no key should be treated as part of the query   
     61    query_args = {} 
     62    for key in options.keys(): 
     63        if not key in AVAILABLE_OPTIONS: 
     64            query_args[key] = options[key] 
     65    return options, query_args 
     66 
     67def execute_query(env, req, query_args): 
     68    query_string = '&'.join(['%s=%s' % item for item in query_args.iteritems()]) 
     69    query = Query.from_string(env, query_string) 
     70 
     71    tickets = query.execute(req) 
     72 
     73    tickets = [t for t in tickets  
     74               if ('TICKET_VIEW' or 'TICKET_VIEW_CC') in req.perm('ticket', t['id'])] 
     75     
     76    return tickets 
     77 
  • estimationtoolsplugin/trunk/estimationtools/workloadchart.py

    r4332 r4448  
    88 
    99class WorkloadChart(WikiMacroBase): 
    10     """Creates workload chart for given milestone
     10    """Creates workload chart for the selected tickets
    1111 
    1212    This macro creates a pie chart that shows the remaining estimated workload per ticket owner, 
    1313    and the remaining work days. 
    1414    It has the following parameters: 
    15      * `milestone`: '''mandatory''' parameter that specifies the milestone.  
     15     * a comma-separated list of query parameters for the ticket selection, in the form "key=value" as specified in TracQuery#QueryLanguage. 
    1616     * `width`: width of resulting diagram (defaults to 400) 
    1717     * `height`: height of resulting diagram (defaults to 100) 
     
    2929     
    3030    def render_macro(self, req, name, content): 
    31         if not (req.perm.has_permission('TICKET_VIEW') or  
    32                 req.perm.has_permission('TICKET_VIEW_CC')): 
    33             raise TracError('TICKET_VIEW or TICKET_VIEW_CC permission required') 
    34         options = copy.copy(DEFAULT_OPTIONS) 
    35         if content: 
    36             for arg in content.split(','): 
    37                 i = arg.index('=') 
    38                 options[arg[:i].strip()] = arg[i+1:].strip() 
    3931        db = self.env.get_db_cnx() 
    40         options = parse_options(db, options) 
    41         milestone = options['milestone'] 
    42         cursor = db.cursor() 
    43         cursor.execute("SELECT owner, p.value " 
    44                        "  FROM ticket t, ticket_custom p" 
    45                        "  WHERE p.ticket = t.id and p.name = %s" 
    46                        "  AND t.milestone = %s", [self.estimation_field, milestone]) 
     32        # prepare options 
     33        options, query_args = parse_options(db, content, copy.copy(DEFAULT_OPTIONS)) 
     34         
     35        query_args[self.estimation_field + "!"] = None 
     36        tickets = execute_query(self.env, req, query_args) 
     37         
    4738        sum = 0.0 
    4839        estimations = {} 
    49         for owner, estimation in cursor
     40        for ticket in tickets
    5041            try: 
    51                 sum += float(estimation) 
     42                estimation = float(ticket[self.estimation_field]) 
     43                owner = ticket['owner'] 
     44                sum += estimation 
    5245                if estimations.has_key(owner): 
    53                     estimations[owner] += float(estimation) 
     46                    estimations[owner] += estimation 
    5447                else: 
    55                     estimations[owner] = float(estimation) 
     48                    estimations[owner] = estimation 
    5649            except: 
    5750                pass 
  • estimationtoolsplugin/trunk/setup.py

    r4361 r4448  
    99    author_email = 'hoessler@gmail.com', 
    1010    description = 'Trac plugin for visualizing and quick editing of effort estimations', 
    11     version = '0.3', 
     11    version = '0.4', 
    1212    license='BSD', 
    1313    packages=['estimationtools'],