Ticket #70: iteration_ticket_and_description-45-70-115.patch

File iteration_ticket_and_description-45-70-115.patch, 22.2 KB (added by ja11sop, 3 years ago)

Unified patch for plugin and patch to add the iteration_ticket table and add the description field to iterations, also add iteration-ticket mapping to roadmap and tickets (updated 14th Oct 2009)

  • patch/trac/ticket/api.py

     
    609609        return ticket_stages 
    610610         
    611611         
     612    def get_ticket_iterations(self, ticket_id, db=None): 
     613        if not db: 
     614            db = self.env.get_db_cnx() 
     615        cursor = db.cursor() 
     616        cursor.execute("SELECT iteration_id FROM iteration_ticket " 
     617                       "WHERE ticket_id=%s ORDER BY iteration_id", (ticket_id,)) 
     618        iteration_ids = [str(row[0]) for row in cursor.fetchall()] 
     619        return iteration_ids 
     620 
     621       
    612622    def get_ticket_info(self, tickets, milestone_sizing_stats, end_date): 
    613         tickets_with_stages = {}         
     623        tickets_with_stages = {} 
    614624        ticket_ids = [t['id'] for t in tickets] 
    615625         
    616626        stages = self.get_completion_stage_details_for_ticket_group(ticket_ids, milestone_sizing_stats, end_date) 
     
    623633        last_modified = None 
    624634        for ticket in tickets: 
    625635            ordered_stages = self.get_ticket_completion_stages(ticket) 
     636            iterations = self.get_ticket_iterations(ticket.id) 
    626637            component = ticket[ 'component' ] and ticket[ 'component' ] or '' 
    627             tickets_with_stages.setdefault( component, {} )[ticket.id] = { 'ticket':ticket, 'ordered_stages':ordered_stages } 
     638            tickets_with_stages.setdefault( component, {} )[ticket.id] = { 'ticket':ticket, 'ordered_stages':ordered_stages, 'iterations':iterations } 
    628639            total_tickets += 1 
    629640             
    630641            id = ticket.id 
     
    694705    def get_tickets_for_iteration(self, db, iteration_tickets): 
    695706        from trac.ticket import Ticket 
    696707        tickets = [] 
    697         for ticket_id in iteration_tickets.split(): 
     708        for ticket_id in iteration_tickets: 
    698709            ticket = Ticket(self.env, ticket_id, db) 
    699710            if ticket.exists: 
    700711               tickets.append(ticket) 
     
    710721         
    711722        cursor = db.cursor() 
    712723        cursor.execute("SELECT ticket,time,author,field,oldvalue,newvalue " 
    713                        "FROM ticket_change WHERE time > %s AND time < %s " 
    714                        "ORDER BY ticket", (start_date, end_date)) 
     724                       "FROM ticket_change, iteration_ticket " 
     725                       "WHERE time > %s AND time < %s " 
     726                       "AND ticket_change.ticket = iteration_ticket.ticket_id " 
     727                       "AND iteration_ticket.iteration_id = %s " 
     728                       "ORDER BY ticket", (start_date, end_date, iteration.id)) 
    715729         
    716730        for ticket, time, author, field, oldvalue, newvalue in cursor: 
    717731            if not changed_tickets.has_key( ticket ): 
  • patch/trac/ticket/web_ui.py

     
    601601                'context': Context.from_request(req, ticket.resource, 
    602602                                                absurls=absurls), 
    603603                'preserve_newlines': self.must_preserve_newlines, 
    604                 'date_hint': get_date_format_hint()} 
     604                'date_hint': get_date_format_hint(), 
     605                'iterations': TicketSystem(self.env).get_ticket_iterations(ticket.id)} 
    605606 
    606607    def _toggle_cc(self, req, cc): 
    607608        """Return an (action, recipient) tuple corresponding to a change 
  • patch/trac/ticket/model.py

     
    981981            self._old_id = iteration_id 
    982982        else: 
    983983            self.id = self._old_id = self.start_date = self.end_date = None 
    984             self.summary = self.tickets = '' 
     984            self.summary = '' 
     985            self.description = '' 
     986            self.tickets = [] 
    985987 
    986988    def _get_resource(self): 
    987989        return Resource('iteration', self.id) ### .version !!! 
     
    991993        if not db: 
    992994            db = self.env.get_db_cnx() 
    993995        cursor = db.cursor() 
    994         cursor.execute("SELECT id,summary,start_date,end_date,tickets " 
     996        cursor.execute("SELECT id,summary,description,start_date,end_date " 
    995997                       "FROM iteration WHERE id=%s", (iteration_id,)) 
    996998        row = cursor.fetchone() 
    997999        if not row: 
    9981000            raise ResourceNotFound('Iteration %s does not exist.' % iteration_id, 
    9991001                                   'Invalid Iteration Id') 
    1000         id, summary, start_date, end_date, tickets = row 
     1002        id, summary, description, start_date, end_date = row 
    10011003        self.id = id 
    10021004        self.summary = summary or '' 
     1005        self.description = description or '' 
    10031006        self.start_date = start_date and datetime.fromtimestamp(int(start_date), utc) or None 
    10041007        self.end_date = end_date and datetime.fromtimestamp(int(end_date), utc) or None 
    1005         self.tickets = tickets or '' 
     1008        cursor.execute("SELECT ticket_id FROM iteration_ticket " 
     1009                       "WHERE iteration_id=%s ORDER BY ticket_id", (iteration_id,)) 
     1010        self.tickets = [str(row[0]) for row in cursor.fetchall()] 
    10061011 
    10071012    is_started = property(fget=lambda self: self.start_date and \ 
    10081013                                            self.start_date.date() < date.today()) 
     
    10241029        cursor = db.cursor() 
    10251030        self.env.log.info('Deleting iteration %s' % self.id) 
    10261031        cursor.execute("DELETE FROM iteration WHERE id=%s", (self.id,)) 
     1032        cursor.execute("DELETE FROM iteration_ticket WHERE iteration_id=%s" % (self.id,)) 
    10271033 
    10281034        if handle_ta: 
    10291035            db.commit() 
     
    10371043            handle_ta = False 
    10381044 
    10391045        cursor = db.cursor() 
    1040         cursor.execute("INSERT INTO iteration (summary,start_date,end_date,tickets) " 
    1041                        "VALUES (%s,%s,%s,%s)", 
    1042                        (self.summary, to_timestamp(self.start_date), to_timestamp(self.end_date), 
    1043                         self.tickets)) 
     1046        cursor.execute("INSERT INTO iteration (summary,description,start_date,end_date) " 
     1047                       "VALUES (%s,%s,%s)", 
     1048                       (self.summary, self.description, to_timestamp(self.start_date), to_timestamp(self.end_date))) 
    10441049        iteration_id = db.get_last_id(cursor, 'iteration') 
     1050        for ticket_id in self.tickets: 
     1051            cursor.execute("INSERT INTO iteration_ticket (iteration_id, ticket_id) " 
     1052                           "VALUES (%s,%s)", 
     1053                           (iteration_id, ticket_id)) 
    10451054 
    10461055        if handle_ta: 
    10471056            db.commit() 
     
    10591068 
    10601069        cursor = db.cursor() 
    10611070        self.env.log.info('Updating iteration "%s"' % self.id) 
    1062         cursor.execute("UPDATE iteration SET summary=%s,start_date=%s," 
    1063                        "end_date=%s,tickets=%s WHERE id=%s", 
    1064                        (self.summary, to_timestamp(self.start_date), to_timestamp(self.end_date), 
    1065                         self.tickets, 
     1071        cursor.execute("UPDATE iteration SET summary=%s,description=%s,start_date=%s," 
     1072                       "end_date=%s WHERE id=%s", 
     1073                       (self.summary, self.description, to_timestamp(self.start_date), to_timestamp(self.end_date), 
    10661074                        self.id)) 
    10671075 
     1076        cursor.execute("DELETE FROM iteration_ticket WHERE iteration_id=%s" % (self.id,)) 
     1077        for ticket_id in self.tickets: 
     1078            cursor.execute("INSERT INTO iteration_ticket (iteration_id, ticket_id) " 
     1079                           "VALUES (%s,%s)", 
     1080                           (self.id, ticket_id)) 
     1081 
    10681082        if handle_ta: 
    10691083            db.commit() 
    10701084 
     
    10721086        if not db: 
    10731087            db = env.get_db_cnx() 
    10741088        now = to_timestamp( datetime.now(utc) - timedelta(days=1) ) 
    1075         sql = "SELECT id,summary,start_date,end_date,tickets FROM iteration " 
     1089        sql = "SELECT id,summary,description,start_date,end_date FROM iteration " 
    10761090        if not include_finished: 
    10771091            sql += "WHERE end_date >= %s " % now 
    10781092        cursor = db.cursor() 
    10791093        cursor.execute(sql) 
     1094        iteration_rows = cursor.fetchall() 
    10801095        iterations = [] 
    1081         for iteration_id,summary,start_date,end_date,tickets in cursor: 
     1096        for iteration_id,summary,description,start_date,end_date in iteration_rows: 
    10821097            iteration = Iteration(env) 
    10831098            iteration.id = iteration._old_id = iteration_id 
    10841099            iteration.summary = summary or '' 
     1100            iteration.description = description or '' 
    10851101            iteration.start_date = start_date and datetime.fromtimestamp(int(start_date), utc) or None 
    10861102            iteration.end_date = end_date and datetime.fromtimestamp(int(end_date), utc) or None 
    1087             iteration.tickets = tickets or '' 
     1103            cursor.execute("SELECT ticket_id FROM iteration_ticket " 
     1104                                  "WHERE iteration_id = %d ORDER BY ticket_id" % (iteration_id,)) 
     1105            iteration.tickets = [str(row[0]) for row in cursor.fetchall()] 
    10881106            iterations.append(iteration) 
    10891107        def iteration_order(m): 
    10901108            return (m.start_date or utcmax, 
  • patch/trac/ticket/roadmap.py

     
    502502            req.perm(iteration.resource).require('ITERATION_CREATE') 
    503503 
    504504        iteration.summary = req.args.get('summary', '') 
     505        iteration.description = req.args.get('description', '') 
    505506 
    506507        start_date = req.args.get('start_date', '') 
    507508        iteration.start_date = start_date and parse_date(start_date, tzinfo=req.tz) or None 
     
    525526                    ticket_tokens.append(id_token) 
    526527            except ValueError: 
    527528                invalid_ticket_tokens.append(id_token) 
    528         iteration_tickets = " ".join(ticket_tokens) 
    529         iteration.tickets = iteration_tickets 
     529        iteration.tickets = ticket_tokens 
    530530        # Instead of raising one single error, check all the constraints and 
    531531        # let the user fix them by going back to edit mode showing the warnings 
    532532        warnings = [] 
  • patch/trac/ticket/query.py

     
    404404 
    405405        sql = [] 
    406406        sql.append("SELECT " + ",".join(['t.%s AS %s' % (c, c) for c in cols 
    407                                          if c not in custom_fields])) 
     407                                         if c not in custom_fields and c != 'iteration'])) 
    408408        sql.append(",priority.value AS priority_value") 
    409409        for k in [k for k in cols if k in custom_fields]: 
    410410            sql.append(",%s.value AS %s" % (k, k)) 
     
    429429                       % (col, col, col)) 
    430430 
    431431        def get_constraint_sql(name, value, mode, neg): 
     432            if name == "iteration": 
     433                name = 'iteration_ticket.iteration_id' 
     434                if mode != '': 
     435                    raise ValueError("Iteration searching only supports is or is not") 
     436                clause = "(SELECT DISTINCT ticket_id FROM iteration_ticket WHERE %s = %%s)" % (name,) 
     437                if neg: 
     438                    clause = "t.id NOT IN %s" % (clause,) 
     439                else: 
     440                    clause = "t.id IN %s" % (clause,) 
     441                value = int(value) 
     442                return (clause, value) 
    432443            if name not in custom_fields: 
    433444                name = 't.' + name 
    434445            else: 
     
    486497                                               ' OR '.join(id_clauses))) 
    487498            # Special case for exact matches on multiple values 
    488499            elif not mode and len(v) > 1: 
    489                 if k not in custom_fields: 
     500                if k == 'iteration': 
     501                    col = 'iteration_ticket.iteration_id' 
     502                elif k not in custom_fields: 
    490503                    col = 't.' + k 
    491504                else: 
    492505                    col = k + '.value' 
    493                 clauses.append("COALESCE(%s,'') %sIN (%s)" 
    494                                % (col, neg and 'NOT ' or '', 
    495                                   ','.join(['%s' for val in v]))) 
    496                 args += [val[neg:] for val in v] 
     506                if k == 'iteration': 
     507                    clause = "%s IN (%s)" % (col, ','.join(['%s' for val in v])) 
     508                    clause = "(SELECT DISTINCT ticket_id FROM iteration_ticket WHERE %s)" % (clause,) 
     509                    clause = "t.id %sIN %s" % (neg and 'NOT ' or '', clause) 
     510                    clauses.append(clause) 
     511                    args += [int(val[neg:]) for val in v] 
     512                else: 
     513                    clauses.append("COALESCE(%s,'') %sIN (%s)" 
     514                                  % (col, neg and 'NOT ' or '', 
     515                                     ','.join(['%s' for val in v]))) 
     516                    args += [val[neg:] for val in v] 
    497517            elif len(v) > 1: 
    498518                constraint_sql = filter(None, 
    499519                                        [get_constraint_sql(k, val, mode, neg) 
     
    585605        # TODO: remove after adding time/changetime to the api.py 
    586606        labels['changetime'] = _('Modified') 
    587607        labels['time'] = _('Created') 
     608        labels['iteration'] = _('Iteration') 
    588609 
    589610        headers = [{ 
    590611            'name': col, 'label': labels.get(col, _('Ticket')), 
     
    602623            field_data.update(field) 
    603624            del field_data['name'] 
    604625            fields[field['name']] = field_data 
    605  
     626        fields['iteration'] = {'label': _('Iteration'), 'type': 'text', 'name': 'iteration'} 
    606627        modes = {} 
    607628        modes['text'] = [ 
    608629            {'name': _("contains"), 'value': "~"}, 
     
    840861        ticket_fields = [f['name'] for f in 
    841862                         TicketSystem(self.env).get_ticket_fields()] 
    842863        ticket_fields.append('id') 
     864        ticket_fields.append('iteration') 
    843865 
    844866        # For clients without JavaScript, we remove constraints here if 
    845867        # requested 
  • patch/trac/ticket/templates/iterations.html

     
    105105              ${progress_bar(iteration_info['iteration_stats'].stats, iteration_info['iteration_stats'].interval_hrefs, stats_href=iteration_info['iteration_stats'].stats_href)} 
    106106            </py:if> 
    107107          </div> 
    108            
     108 
     109          <div class="description" xml:space="preserve"> 
     110            ${wiki_to_html(context, iteration.description)} 
     111          </div> 
     112 
    109113          ${ticket_work_table(context(iteration.resource), iterations_info[idx]['ticket_info'], completion_stages)} 
    110114        </li> 
    111115      </ul> 
    112116       
    113       <div py:if="'MILESTONE_CREATE' in perm" class="buttons"> 
     117      <div py:if="'ITERATION_CREATE' in perm" class="buttons"> 
    114118       <form method="get" action="${href.iteration()}"><div> 
    115119        <input type="hidden" name="action" value="new" /> 
    116120        <input type="submit" value="Add new iteration" /> 
  • patch/trac/ticket/templates/ticket.html

     
    265265                </td> 
    266266              </py:for> 
    267267            </tr> 
     268            <tr class="iterations_row"> 
     269              <th>In Iterations:</th> 
     270              <py:choose test=""> 
     271                <py:when test="len(iterations) == 0"> 
     272                  <td class="no_iterations">None</td> 
     273                </py:when> 
     274                <py:otherwise> 
     275                  <td class="iterations"> 
     276                    <py:for each="iteration in iterations"> 
     277                      <a href="${href.iteration(iteration)}">${iteration}</a> 
     278                    </py:for> 
     279                  </td> 
     280                </py:otherwise> 
     281              </py:choose> 
     282            </tr> 
    268283          </table> 
    269284        </div> 
    270285        <h2 id="comment:description"> 
  • patch/trac/ticket/templates/iteration_view.html

     
    4646          </py:with> 
    4747        </div> 
    4848      </fieldset> 
     49 
     50      <div class="description" xml:space="preserve"> 
     51        ${wiki_to_html(context, iteration.description)} 
     52      </div> 
     53 
    4954      <py:with vars="ticket_info=iteration_info['ticket_info']"> 
    5055        ${ticket_work_table(context, ticket_info, completion_stages)} 
    5156      </py:with> 
  • patch/trac/ticket/templates/iteration_edit.html

     
    212212          <div class="field"> 
    213213            <label>List the tickets, by id, associated with this iteration, separated by a space:<br /> 
    214214            <input type="text" id="iteration_tickets" name="iteration_tickets" size="60" 
    215                    value="${iteration.tickets}" /> 
     215                   value="${' '.join(iteration.tickets)}" /> 
    216216            <em>For example: 56 3 4 12</em> 
    217217            </label> 
    218218          </div> 
     
    228228            ${ticket_work_table(context, ticket_info, completion_stages)} 
    229229          </py:with> 
    230230        </fieldset> 
     231 
     232        <div class="field"> 
     233          <fieldset class="iefix"> 
     234            <label for="description">Description (you may use <a tabindex="42" 
     235                   href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here):</label> 
     236            <p><textarea id="description" name="description" class="wikitext" rows="10" cols="78"> 
     237              ${iteration.description} 
     238            </textarea></p> 
     239          </fieldset> 
     240        </div> 
     241 
    231242        <div class="buttons" py:choose="iteration.exists"> 
    232243          <input py:when="True" type="submit" value="Submit changes" /> 
    233244          <input py:otherwise="" type="submit" value="Add iteration" /> 
  • patch/trac/htdocs/css/ticket.css

     
    7474 
    7575#ticket table.properties .relative_size { font-weight: bold; font-size: 100%; } 
    7676#ticket table.properties .completion_date { font-weight: bold; font-size: 100%; } 
     77#ticket table.properties .iterations_row { border-top: 1px solid #dd9 } 
     78#ticket table.properties .iterations_row .no_iterations { font-weight: bold; font-size: 100%; } 
     79#ticket table.properties .iterations_row .iterations { font-size: 100%; } 
    7780 
    7881div#ticket.description h2 { 
    7982 border-bottom: 1px solid #d7d7d7; 
     
    114117form .field fieldset.iefix { margin-left: 1px; margin-right: 1px } 
    115118form .field #comment { margin-left: -1px; margin-right: -1px; padding: 0; width: 100% } 
    116119form .field .wikitoolbar { margin-left: -1px } 
     120form .field .iterations { margin-left: -1px } 
    117121 
    118122#properties { white-space: nowrap; line-height: 160%; padding: .5em } 
    119123#properties table { border-spacing: 0; width: 100%; } 
  • patch/trac/admin/templates/admin_iterations.html

     
    5050            <div class="field"> 
    5151              <label>List the tickets, by id, associated with this iteration, separated by a space:<br /> 
    5252              <input type="text" id="iteration_tickets" name="iteration_tickets" size="60" 
    53                     value="${iteration.tickets}" /> 
     53                    value="${' '.join(iteration.tickets)}" /> 
    5454              <em>For example: 56 3 4 12</em> 
    5555              </label> 
    5656            </div> 
     
    122122                <td><py:if test="iteration.end_date"> 
    123123                  ${format_date(iteration.end_date)} 
    124124                </py:if></td> 
    125                 <td>${iteration.tickets}</td> 
     125                <td>${' '.join(iteration.tickets)}</td> 
    126126              </tr></tbody> 
    127127            </table> 
    128128            <div class="buttons"> 
  • patch/trac/templates/macros.html

     
    271271        <tr> 
    272272          <th rowspan="2" class="id">Id</th> 
    273273          <th rowspan="2">Summary</th> 
     274          <th rowspan="2">Iterations</th> 
    274275          <py:for each="stage in completion_stages"> 
    275276              <th colspan="2" class="completion_stage">${stage['short_label']}</th> 
    276277          </py:for> 
     
    296297              <td><a href="${href.ticket(ticket.id)}">#${ticket.id}</a></td> 
    297298              <td class="summary" xml:space="preserve"> 
    298299                ${wiki_to_oneliner(context, ticket['summary'])} 
    299               </td>  
     300              </td> 
     301              <td class="iterations" > 
     302                <py:for each="iteration in ticket_with_stages['iterations']"> 
     303                  <a href="${href.iteration(iteration)}">${iteration}</a> 
     304                </py:for> 
     305              </td> 
    300306                <py:for each="stage in ticket_with_stages['ordered_stages']"> 
    301307                  <td py:with="ticket_stage=ticket_stages[stage['stage']]"> 
    302308                    <py:choose> 
  • setup.py

     
     1# -*- coding: utf-8 -*- 
    12from setuptools import find_packages, setup 
    23 
    34setup( 
    4     name='Agile-Trac', version='0.2.1.dev', 
     5    name='Agile-Trac', version='0.2.2.dev', 
    56    author = 'ja11sop', 
    67    author_email = 'ja11sop@agile-trac.org', 
    78    url = 'http://www.agile-trac.org/', 
  • agiletrac/env.py

     
    22from trac.env import IEnvironmentSetupParticipant 
    33from trac.db import * 
    44 
    5 current_db_version = 1 
     5current_db_version = 2 
    66 
    77class AgileTracSetup(Component): 
    88    """Central functionality for the Agile-Trac plugin."""