Index: patch/trac/ticket/api.py
===================================================================
--- patch/trac/ticket/api.py	(revision 309)
+++ patch/trac/ticket/api.py	(working copy)
@@ -609,8 +609,18 @@
         return ticket_stages
         
         
+    def get_ticket_iterations(self, ticket_id, db=None):
+        if not db:
+            db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT iteration_id FROM iteration_ticket "
+                       "WHERE ticket_id=%s ORDER BY iteration_id", (ticket_id,))
+        iteration_ids = [str(row[0]) for row in cursor.fetchall()]
+        return iteration_ids
+
+      
     def get_ticket_info(self, tickets, milestone_sizing_stats, end_date):
-        tickets_with_stages = {}        
+        tickets_with_stages = {}
         ticket_ids = [t['id'] for t in tickets]
         
         stages = self.get_completion_stage_details_for_ticket_group(ticket_ids, milestone_sizing_stats, end_date)
@@ -623,8 +633,9 @@
         last_modified = None
         for ticket in tickets:
             ordered_stages = self.get_ticket_completion_stages(ticket)
+            iterations = self.get_ticket_iterations(ticket.id)
             component = ticket[ 'component' ] and ticket[ 'component' ] or ''
-            tickets_with_stages.setdefault( component, {} )[ticket.id] = { 'ticket':ticket, 'ordered_stages':ordered_stages }
+            tickets_with_stages.setdefault( component, {} )[ticket.id] = { 'ticket':ticket, 'ordered_stages':ordered_stages, 'iterations':iterations }
             total_tickets += 1
             
             id = ticket.id
@@ -694,7 +705,7 @@
     def get_tickets_for_iteration(self, db, iteration_tickets):
         from trac.ticket import Ticket
         tickets = []
-        for ticket_id in iteration_tickets.split():
+        for ticket_id in iteration_tickets:
             ticket = Ticket(self.env, ticket_id, db)
             if ticket.exists:
                tickets.append(ticket)
@@ -710,8 +721,11 @@
         
         cursor = db.cursor()
         cursor.execute("SELECT ticket,time,author,field,oldvalue,newvalue "
-                       "FROM ticket_change WHERE time > %s AND time < %s "
-                       "ORDER BY ticket", (start_date, end_date))
+                       "FROM ticket_change, iteration_ticket "
+                       "WHERE time > %s AND time < %s "
+                       "AND ticket_change.ticket = iteration_ticket.ticket_id "
+                       "AND iteration_ticket.iteration_id = %s "
+                       "ORDER BY ticket", (start_date, end_date, iteration.id))
         
         for ticket, time, author, field, oldvalue, newvalue in cursor:
             if not changed_tickets.has_key( ticket ):
Index: patch/trac/ticket/web_ui.py
===================================================================
--- patch/trac/ticket/web_ui.py	(revision 309)
+++ patch/trac/ticket/web_ui.py	(working copy)
@@ -601,7 +601,8 @@
                 'context': Context.from_request(req, ticket.resource,
                                                 absurls=absurls),
                 'preserve_newlines': self.must_preserve_newlines,
-                'date_hint': get_date_format_hint()}
+                'date_hint': get_date_format_hint(),
+                'iterations': TicketSystem(self.env).get_ticket_iterations(ticket.id)}
 
     def _toggle_cc(self, req, cc):
         """Return an (action, recipient) tuple corresponding to a change
Index: patch/trac/ticket/model.py
===================================================================
--- patch/trac/ticket/model.py	(revision 309)
+++ patch/trac/ticket/model.py	(working copy)
@@ -981,7 +981,9 @@
             self._old_id = iteration_id
         else:
             self.id = self._old_id = self.start_date = self.end_date = None
-            self.summary = self.tickets = ''
+            self.summary = ''
+            self.description = ''
+            self.tickets = []
 
     def _get_resource(self):
         return Resource('iteration', self.id) ### .version !!!
@@ -991,18 +993,21 @@
         if not db:
             db = self.env.get_db_cnx()
         cursor = db.cursor()
-        cursor.execute("SELECT id,summary,start_date,end_date,tickets "
+        cursor.execute("SELECT id,summary,description,start_date,end_date "
                        "FROM iteration WHERE id=%s", (iteration_id,))
         row = cursor.fetchone()
         if not row:
             raise ResourceNotFound('Iteration %s does not exist.' % iteration_id,
                                    'Invalid Iteration Id')
-        id, summary, start_date, end_date, tickets = row
+        id, summary, description, start_date, end_date = row
         self.id = id
         self.summary = summary or ''
+        self.description = description or ''
         self.start_date = start_date and datetime.fromtimestamp(int(start_date), utc) or None
         self.end_date = end_date and datetime.fromtimestamp(int(end_date), utc) or None
-        self.tickets = tickets or ''
+        cursor.execute("SELECT ticket_id FROM iteration_ticket "
+                       "WHERE iteration_id=%s ORDER BY ticket_id", (iteration_id,))
+        self.tickets = [str(row[0]) for row in cursor.fetchall()]
 
     is_started = property(fget=lambda self: self.start_date and \
                                             self.start_date.date() < date.today())
@@ -1024,6 +1029,7 @@
         cursor = db.cursor()
         self.env.log.info('Deleting iteration %s' % self.id)
         cursor.execute("DELETE FROM iteration WHERE id=%s", (self.id,))
+        cursor.execute("DELETE FROM iteration_ticket WHERE iteration_id=%s" % (self.id,))
 
         if handle_ta:
             db.commit()
@@ -1037,11 +1043,14 @@
             handle_ta = False
 
         cursor = db.cursor()
-        cursor.execute("INSERT INTO iteration (summary,start_date,end_date,tickets) "
-                       "VALUES (%s,%s,%s,%s)",
-                       (self.summary, to_timestamp(self.start_date), to_timestamp(self.end_date),
-                        self.tickets))
+        cursor.execute("INSERT INTO iteration (summary,description,start_date,end_date) "
+                       "VALUES (%s,%s,%s)",
+                       (self.summary, self.description, to_timestamp(self.start_date), to_timestamp(self.end_date)))
         iteration_id = db.get_last_id(cursor, 'iteration')
+        for ticket_id in self.tickets:
+            cursor.execute("INSERT INTO iteration_ticket (iteration_id, ticket_id) "
+                           "VALUES (%s,%s)",
+                           (iteration_id, ticket_id))
 
         if handle_ta:
             db.commit()
@@ -1059,12 +1068,17 @@
 
         cursor = db.cursor()
         self.env.log.info('Updating iteration "%s"' % self.id)
-        cursor.execute("UPDATE iteration SET summary=%s,start_date=%s,"
-                       "end_date=%s,tickets=%s WHERE id=%s",
-                       (self.summary, to_timestamp(self.start_date), to_timestamp(self.end_date),
-                        self.tickets,
+        cursor.execute("UPDATE iteration SET summary=%s,description=%s,start_date=%s,"
+                       "end_date=%s WHERE id=%s",
+                       (self.summary, self.description, to_timestamp(self.start_date), to_timestamp(self.end_date),
                         self.id))
 
+        cursor.execute("DELETE FROM iteration_ticket WHERE iteration_id=%s" % (self.id,))
+        for ticket_id in self.tickets:
+            cursor.execute("INSERT INTO iteration_ticket (iteration_id, ticket_id) "
+                           "VALUES (%s,%s)",
+                           (self.id, ticket_id))
+
         if handle_ta:
             db.commit()
 
@@ -1072,19 +1086,23 @@
         if not db:
             db = env.get_db_cnx()
         now = to_timestamp( datetime.now(utc) - timedelta(days=1) )
-        sql = "SELECT id,summary,start_date,end_date,tickets FROM iteration "
+        sql = "SELECT id,summary,description,start_date,end_date FROM iteration "
         if not include_finished:
             sql += "WHERE end_date >= %s " % now
         cursor = db.cursor()
         cursor.execute(sql)
+        iteration_rows = cursor.fetchall()
         iterations = []
-        for iteration_id,summary,start_date,end_date,tickets in cursor:
+        for iteration_id,summary,description,start_date,end_date in iteration_rows:
             iteration = Iteration(env)
             iteration.id = iteration._old_id = iteration_id
             iteration.summary = summary or ''
+            iteration.description = description or ''
             iteration.start_date = start_date and datetime.fromtimestamp(int(start_date), utc) or None
             iteration.end_date = end_date and datetime.fromtimestamp(int(end_date), utc) or None
-            iteration.tickets = tickets or ''
+            cursor.execute("SELECT ticket_id FROM iteration_ticket "
+                                  "WHERE iteration_id = %d ORDER BY ticket_id" % (iteration_id,))
+            iteration.tickets = [str(row[0]) for row in cursor.fetchall()]
             iterations.append(iteration)
         def iteration_order(m):
             return (m.start_date or utcmax,
Index: patch/trac/ticket/roadmap.py
===================================================================
--- patch/trac/ticket/roadmap.py	(revision 309)
+++ patch/trac/ticket/roadmap.py	(working copy)
@@ -502,6 +502,7 @@
             req.perm(iteration.resource).require('ITERATION_CREATE')
 
         iteration.summary = req.args.get('summary', '')
+        iteration.description = req.args.get('description', '')
 
         start_date = req.args.get('start_date', '')
         iteration.start_date = start_date and parse_date(start_date, tzinfo=req.tz) or None
@@ -525,8 +526,7 @@
                     ticket_tokens.append(id_token)
             except ValueError:
                 invalid_ticket_tokens.append(id_token)
-        iteration_tickets = " ".join(ticket_tokens)
-        iteration.tickets = iteration_tickets
+        iteration.tickets = ticket_tokens
         # Instead of raising one single error, check all the constraints and
         # let the user fix them by going back to edit mode showing the warnings
         warnings = []
Index: patch/trac/ticket/query.py
===================================================================
--- patch/trac/ticket/query.py	(revision 309)
+++ patch/trac/ticket/query.py	(working copy)
@@ -404,7 +404,7 @@
 
         sql = []
         sql.append("SELECT " + ",".join(['t.%s AS %s' % (c, c) for c in cols
-                                         if c not in custom_fields]))
+                                         if c not in custom_fields and c != 'iteration']))
         sql.append(",priority.value AS priority_value")
         for k in [k for k in cols if k in custom_fields]:
             sql.append(",%s.value AS %s" % (k, k))
@@ -429,6 +429,17 @@
                        % (col, col, col))
 
         def get_constraint_sql(name, value, mode, neg):
+            if name == "iteration":
+                name = 'iteration_ticket.iteration_id'
+                if mode != '':
+                    raise ValueError("Iteration searching only supports is or is not")
+                clause = "(SELECT DISTINCT ticket_id FROM iteration_ticket WHERE %s = %%s)" % (name,)
+                if neg:
+                    clause = "t.id NOT IN %s" % (clause,)
+                else:
+                    clause = "t.id IN %s" % (clause,)
+                value = int(value)
+                return (clause, value)
             if name not in custom_fields:
                 name = 't.' + name
             else:
@@ -486,14 +497,23 @@
                                                ' OR '.join(id_clauses)))
             # Special case for exact matches on multiple values
             elif not mode and len(v) > 1:
-                if k not in custom_fields:
+                if k == 'iteration':
+                    col = 'iteration_ticket.iteration_id'
+                elif k not in custom_fields:
                     col = 't.' + k
                 else:
                     col = k + '.value'
-                clauses.append("COALESCE(%s,'') %sIN (%s)"
-                               % (col, neg and 'NOT ' or '',
-                                  ','.join(['%s' for val in v])))
-                args += [val[neg:] for val in v]
+                if k == 'iteration':
+                    clause = "%s IN (%s)" % (col, ','.join(['%s' for val in v]))
+                    clause = "(SELECT DISTINCT ticket_id FROM iteration_ticket WHERE %s)" % (clause,)
+                    clause = "t.id %sIN %s" % (neg and 'NOT ' or '', clause)
+                    clauses.append(clause)
+                    args += [int(val[neg:]) for val in v]
+                else:
+                    clauses.append("COALESCE(%s,'') %sIN (%s)"
+                                  % (col, neg and 'NOT ' or '',
+                                     ','.join(['%s' for val in v])))
+                    args += [val[neg:] for val in v]
             elif len(v) > 1:
                 constraint_sql = filter(None,
                                         [get_constraint_sql(k, val, mode, neg)
@@ -585,6 +605,7 @@
         # TODO: remove after adding time/changetime to the api.py
         labels['changetime'] = _('Modified')
         labels['time'] = _('Created')
+        labels['iteration'] = _('Iteration')
 
         headers = [{
             'name': col, 'label': labels.get(col, _('Ticket')),
@@ -602,7 +623,7 @@
             field_data.update(field)
             del field_data['name']
             fields[field['name']] = field_data
-
+        fields['iteration'] = {'label': _('Iteration'), 'type': 'text', 'name': 'iteration'}
         modes = {}
         modes['text'] = [
             {'name': _("contains"), 'value': "~"},
@@ -840,6 +861,7 @@
         ticket_fields = [f['name'] for f in
                          TicketSystem(self.env).get_ticket_fields()]
         ticket_fields.append('id')
+        ticket_fields.append('iteration')
 
         # For clients without JavaScript, we remove constraints here if
         # requested
Index: patch/trac/ticket/templates/iterations.html
===================================================================
--- patch/trac/ticket/templates/iterations.html	(revision 309)
+++ patch/trac/ticket/templates/iterations.html	(working copy)
@@ -105,12 +105,16 @@
               ${progress_bar(iteration_info['iteration_stats'].stats, iteration_info['iteration_stats'].interval_hrefs, stats_href=iteration_info['iteration_stats'].stats_href)}
             </py:if>
           </div>
-          
+
+          <div class="description" xml:space="preserve">
+            ${wiki_to_html(context, iteration.description)}
+          </div>
+
           ${ticket_work_table(context(iteration.resource), iterations_info[idx]['ticket_info'], completion_stages)}
         </li>
       </ul>
       
-      <div py:if="'MILESTONE_CREATE' in perm" class="buttons">
+      <div py:if="'ITERATION_CREATE' in perm" class="buttons">
        <form method="get" action="${href.iteration()}"><div>
         <input type="hidden" name="action" value="new" />
         <input type="submit" value="Add new iteration" />
Index: patch/trac/ticket/templates/ticket.html
===================================================================
--- patch/trac/ticket/templates/ticket.html	(revision 309)
+++ patch/trac/ticket/templates/ticket.html	(working copy)
@@ -265,6 +265,21 @@
                 </td>
               </py:for>
             </tr>
+            <tr class="iterations_row">
+              <th>In Iterations:</th>
+              <py:choose test="">
+                <py:when test="len(iterations) == 0">
+                  <td class="no_iterations">None</td>
+                </py:when>
+                <py:otherwise>
+                  <td class="iterations">
+                    <py:for each="iteration in iterations">
+                      <a href="${href.iteration(iteration)}">${iteration}</a>
+                    </py:for>
+                  </td>
+                </py:otherwise>
+              </py:choose>
+            </tr>
           </table>
         </div>
         <h2 id="comment:description">
Index: patch/trac/ticket/templates/iteration_view.html
===================================================================
--- patch/trac/ticket/templates/iteration_view.html	(revision 309)
+++ patch/trac/ticket/templates/iteration_view.html	(working copy)
@@ -46,6 +46,11 @@
           </py:with>
         </div>
       </fieldset>
+
+      <div class="description" xml:space="preserve">
+        ${wiki_to_html(context, iteration.description)}
+      </div>
+
       <py:with vars="ticket_info=iteration_info['ticket_info']">
         ${ticket_work_table(context, ticket_info, completion_stages)}
       </py:with>
Index: patch/trac/ticket/templates/iteration_edit.html
===================================================================
--- patch/trac/ticket/templates/iteration_edit.html	(revision 309)
+++ patch/trac/ticket/templates/iteration_edit.html	(working copy)
@@ -212,7 +212,7 @@
           <div class="field">
             <label>List the tickets, by id, associated with this iteration, separated by a space:<br />
             <input type="text" id="iteration_tickets" name="iteration_tickets" size="60"
-                   value="${iteration.tickets}" />
+                   value="${' '.join(iteration.tickets)}" />
             <em>For example: 56 3 4 12</em>
             </label>
           </div>
@@ -228,6 +228,17 @@
             ${ticket_work_table(context, ticket_info, completion_stages)}
           </py:with>
         </fieldset>
+
+        <div class="field">
+          <fieldset class="iefix">
+            <label for="description">Description (you may use <a tabindex="42"
+                   href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here):</label>
+            <p><textarea id="description" name="description" class="wikitext" rows="10" cols="78">
+              ${iteration.description}
+            </textarea></p>
+          </fieldset>
+        </div>
+
         <div class="buttons" py:choose="iteration.exists">
           <input py:when="True" type="submit" value="Submit changes" />
           <input py:otherwise="" type="submit" value="Add iteration" />
Index: patch/trac/htdocs/css/ticket.css
===================================================================
--- patch/trac/htdocs/css/ticket.css	(revision 309)
+++ patch/trac/htdocs/css/ticket.css	(working copy)
@@ -74,6 +74,9 @@
 
 #ticket table.properties .relative_size { font-weight: bold; font-size: 100%; }
 #ticket table.properties .completion_date { font-weight: bold; font-size: 100%; }
+#ticket table.properties .iterations_row { border-top: 1px solid #dd9 }
+#ticket table.properties .iterations_row .no_iterations { font-weight: bold; font-size: 100%; }
+#ticket table.properties .iterations_row .iterations { font-size: 100%; }
 
 div#ticket.description h2 {
  border-bottom: 1px solid #d7d7d7;
@@ -114,6 +117,7 @@
 form .field fieldset.iefix { margin-left: 1px; margin-right: 1px }
 form .field #comment { margin-left: -1px; margin-right: -1px; padding: 0; width: 100% }
 form .field .wikitoolbar { margin-left: -1px }
+form .field .iterations { margin-left: -1px }
 
 #properties { white-space: nowrap; line-height: 160%; padding: .5em }
 #properties table { border-spacing: 0; width: 100%; }
Index: patch/trac/admin/templates/admin_iterations.html
===================================================================
--- patch/trac/admin/templates/admin_iterations.html	(revision 309)
+++ patch/trac/admin/templates/admin_iterations.html	(working copy)
@@ -50,7 +50,7 @@
             <div class="field">
               <label>List the tickets, by id, associated with this iteration, separated by a space:<br />
               <input type="text" id="iteration_tickets" name="iteration_tickets" size="60"
-                    value="${iteration.tickets}" />
+                    value="${' '.join(iteration.tickets)}" />
               <em>For example: 56 3 4 12</em>
               </label>
             </div>
@@ -122,7 +122,7 @@
                 <td><py:if test="iteration.end_date">
                   ${format_date(iteration.end_date)}
                 </py:if></td>
-                <td>${iteration.tickets}</td>
+                <td>${' '.join(iteration.tickets)}</td>
               </tr></tbody>
             </table>
             <div class="buttons">
Index: patch/trac/templates/macros.html
===================================================================
--- patch/trac/templates/macros.html	(revision 309)
+++ patch/trac/templates/macros.html	(working copy)
@@ -271,6 +271,7 @@
         <tr>
           <th rowspan="2" class="id">Id</th>
           <th rowspan="2">Summary</th>
+          <th rowspan="2">Iterations</th>
           <py:for each="stage in completion_stages">
               <th colspan="2" class="completion_stage">${stage['short_label']}</th>
           </py:for>
@@ -296,7 +297,12 @@
               <td><a href="${href.ticket(ticket.id)}">#${ticket.id}</a></td>
               <td class="summary" xml:space="preserve">
                 ${wiki_to_oneliner(context, ticket['summary'])}
-              </td> 
+              </td>
+              <td class="iterations" >
+                <py:for each="iteration in ticket_with_stages['iterations']">
+                  <a href="${href.iteration(iteration)}">${iteration}</a>
+                </py:for>
+              </td>
                 <py:for each="stage in ticket_with_stages['ordered_stages']">
                   <td py:with="ticket_stage=ticket_stages[stage['stage']]">
                     <py:choose>
Index: setup.py
===================================================================
--- setup.py	(revision 309)
+++ setup.py	(working copy)
@@ -1,7 +1,8 @@
+# -*- coding: utf-8 -*-
 from setuptools import find_packages, setup
 
 setup(
-    name='Agile-Trac', version='0.2.1.dev',
+    name='Agile-Trac', version='0.2.2.dev',
     author = 'ja11sop',
     author_email = 'ja11sop@agile-trac.org',
     url = 'http://www.agile-trac.org/',
Index: agiletrac/env.py
===================================================================
--- agiletrac/env.py	(revision 309)
+++ agiletrac/env.py	(working copy)
@@ -2,7 +2,7 @@
 from trac.env import IEnvironmentSetupParticipant
 from trac.db import *
 
-current_db_version = 1
+current_db_version = 2
 
 class AgileTracSetup(Component):
     """Central functionality for the Agile-Trac plugin."""

