Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/admin.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/admin.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/admin.py 
(original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/admin.py Sat 
Nov 15 01:14:46 2014
@@ -15,7 +15,9 @@ from __future__ import with_statement
 
 from datetime import datetime
 
-from trac.admin import *
+from trac.admin.api import AdminCommandError, IAdminCommandProvider, \
+                           IAdminPanelProvider, console_date_format, \
+                           console_datetime_format, get_console_locale
 from trac.core import *
 from trac.perm import PermissionSystem
 from trac.resource import ResourceNotFound
@@ -40,11 +42,10 @@ class TicketAdminPanel(Component):
     #            and don't use it whenever using them as field names (after
     #            a call to `.lower()`)
 
-
     # IAdminPanelProvider methods
 
     def get_admin_panels(self, req):
-        if 'TICKET_ADMIN' in req.perm:
+        if 'TICKET_ADMIN' in req.perm('admin', 'ticket/' + self._type):
             # in global scope show only products
             # in local scope everything but products
             parent = getattr(self.env, 'parent', None)
@@ -54,7 +55,6 @@ class TicketAdminPanel(Component):
                         gettext(self._label[1]))
 
     def render_admin_panel(self, req, cat, page, version):
-        req.perm.require('TICKET_ADMIN')
         # Trap AssertionErrors and convert them to TracErrors
         try:
             return self._render_admin_panel(req, cat, page, version)
@@ -152,7 +152,7 @@ class ComponentAdminPanel(TicketAdminPan
                         req.redirect(req.href.admin(cat, page))
 
             data = {'view': 'list',
-                    'components': model.Component.select(self.env),
+                    'components': list(model.Component.select(self.env)),
                     'default': default}
 
         if self.config.getbool('ticket', 'restrict_owner'):
@@ -175,7 +175,7 @@ class ComponentAdminPanel(TicketAdminPan
         yield ('component list', '',
                'Show available components',
                None, self._do_list)
-        yield ('component add', '<name> <owner>',
+        yield ('component add', '<name> [owner]',
                'Add a new component',
                self._complete_add, self._do_add)
         yield ('component rename', '<name> <newname>',
@@ -214,7 +214,7 @@ class ComponentAdminPanel(TicketAdminPan
                      for c in model.Component.select(self.env)],
                     [_('Name'), _('Owner')])
 
-    def _do_add(self, name, owner):
+    def _do_add(self, name, owner=None):
         component = model.Component(self.env)
         component.name = name
         component.owner = owner
@@ -242,21 +242,19 @@ class MilestoneAdminPanel(TicketAdminPan
     # IAdminPanelProvider methods
 
     def get_admin_panels(self, req):
-        if 'MILESTONE_VIEW' in req.perm:
+        if 'MILESTONE_VIEW' in req.perm('admin', 'ticket/' + self._type):
             return TicketAdminPanel.get_admin_panels(self, req)
-        return iter([])
 
     # TicketAdminPanel methods
 
     def _render_admin_panel(self, req, cat, page, milestone):
-        req.perm.require('MILESTONE_VIEW')
-
+        perm = req.perm('admin', 'ticket/' + self._type)
         # Detail view?
         if milestone:
             mil = model.Milestone(self.env, milestone)
             if req.method == 'POST':
                 if req.args.get('save'):
-                    req.perm.require('MILESTONE_MODIFY')
+                    perm.require('MILESTONE_MODIFY')
                     mil.name = name = req.args.get('name')
                     mil.due = mil.completed = None
                     due = req.args.get('duedate', '')
@@ -273,7 +271,7 @@ class MilestoneAdminPanel(TicketAdminPan
                                             _('Invalid Completion Date'))
                     mil.description = req.args.get('description', '')
                     try:
-                        mil.update()
+                        mil.update(author=req.authname)
                     except self.env.db_exc.IntegrityError:
                         raise TracError(_('The milestone "%(name)s" already '
                                           'exists.', name=name))
@@ -290,7 +288,7 @@ class MilestoneAdminPanel(TicketAdminPan
             if req.method == 'POST':
                 # Add Milestone
                 if req.args.get('add') and req.args.get('name'):
-                    req.perm.require('MILESTONE_CREATE')
+                    perm.require('MILESTONE_CREATE')
                     name = req.args.get('name')
                     try:
                         mil = model.Milestone(self.env, name=name)
@@ -313,7 +311,7 @@ class MilestoneAdminPanel(TicketAdminPan
 
                 # Remove milestone
                 elif req.args.get('remove'):
-                    req.perm.require('MILESTONE_DELETE')
+                    perm.require('MILESTONE_DELETE')
                     sel = req.args.get('sel')
                     if not sel:
                         raise TracError(_('No milestone selected'))
@@ -357,6 +355,10 @@ class MilestoneAdminPanel(TicketAdminPan
     # IAdminCommandProvider methods
 
     def get_admin_commands(self):
+        hints = {
+           'datetime': get_datetime_format_hint(get_console_locale(self.env)),
+           'iso8601': get_datetime_format_hint('iso8601'),
+        }
         yield ('milestone list', '',
                "Show milestones",
                None, self._do_list)
@@ -369,20 +371,22 @@ class MilestoneAdminPanel(TicketAdminPan
         yield ('milestone due', '<name> <due>',
                """Set milestone due date
 
-               The <due> date must be specified in the "%s" format.
+               The <due> date must be specified in the "%(datetime)s"
+               or "%(iso8601)s" (ISO 8601) format.
                Alternatively, "now" can be used to set the due date to the
                current time. To remove the due date from a milestone, specify
                an empty string ("").
-               """ % console_date_format_hint,
+               """ % hints,
                self._complete_name, self._do_due)
         yield ('milestone completed', '<name> <completed>',
                """Set milestone complete date
 
-               The <completed> date must be specified in the "%s" format.
+               The <completed> date must be specified in the "%(datetime)s"
+               or "%(iso8601)s" (ISO 8601) format.
                Alternatively, "now" can be used to set the completion date to
                the current time. To remove the completion date from a
                milestone, specify an empty string ("").
-               """ % console_date_format_hint,
+               """ % hints,
                self._complete_name, self._do_completed)
         yield ('milestone remove', '<name>',
                "Remove milestone",
@@ -396,10 +400,11 @@ class MilestoneAdminPanel(TicketAdminPan
             return self.get_milestone_list()
 
     def _do_list(self):
-        print_table([(m.name, m.due and
-                        format_date(m.due, console_date_format),
-                      m.completed and
-                        format_datetime(m.completed, console_datetime_format))
+        print_table([(m.name,
+                      format_date(m.due, console_date_format)
+                      if m.due else None,
+                      format_datetime(m.completed, console_datetime_format)
+                      if m.completed else None)
                      for m in model.Milestone.select(self.env)],
                     [_("Name"), _("Due"), _("Completed")])
 
@@ -407,23 +412,27 @@ class MilestoneAdminPanel(TicketAdminPan
         milestone = model.Milestone(self.env)
         milestone.name = name
         if due is not None:
-            milestone.due = parse_date(due, hint='datetime')
+            milestone.due = parse_date(due, hint='datetime',
+                                       locale=get_console_locale(self.env))
         milestone.insert()
 
     def _do_rename(self, name, newname):
         milestone = model.Milestone(self.env, name)
         milestone.name = newname
-        milestone.update()
+        milestone.update(author=getuser())
 
     def _do_due(self, name, due):
         milestone = model.Milestone(self.env, name)
-        milestone.due = due and parse_date(due, hint='datetime')
+        milestone.due = parse_date(due, hint='datetime',
+                                   locale=get_console_locale(self.env)) \
+                        if due else None
         milestone.update()
 
     def _do_completed(self, name, completed):
         milestone = model.Milestone(self.env, name)
-        milestone.completed = completed and parse_date(completed,
-                                                       hint='datetime')
+        milestone.completed = parse_date(completed, hint='datetime',
+                                         locale=get_console_locale(self.env)) \
+                              if completed else None
         milestone.update()
 
     def _do_remove(self, name):
@@ -515,7 +524,7 @@ class VersionAdminPanel(TicketAdminPanel
                         req.redirect(req.href.admin(cat, page))
 
             data = {'view': 'list',
-                    'versions': model.Version.select(self.env),
+                    'versions': list(model.Version.select(self.env)),
                     'default': default}
 
         Chrome(self.env).add_jquery_ui(req)
@@ -528,6 +537,10 @@ class VersionAdminPanel(TicketAdminPanel
     # IAdminCommandProvider methods
 
     def get_admin_commands(self):
+        hints = {
+           'datetime': get_datetime_format_hint(get_console_locale(self.env)),
+           'iso8601': get_datetime_format_hint('iso8601'),
+        }
         yield ('version list', '',
                "Show versions",
                None, self._do_list)
@@ -540,11 +553,12 @@ class VersionAdminPanel(TicketAdminPanel
         yield ('version time', '<name> <time>',
                """Set version date
 
-               The <time> must be specified in the "%s" format. Alternatively,
-               "now" can be used to set the version date to the current time.
-               To remove the date from a version, specify an empty string
-               ("").
-               """ % console_date_format_hint,
+               The <time> must be specified in the "%(datetime)s"
+               or "%(iso8601)s" (ISO 8601) format.
+               Alternatively, "now" can be used to set the version date to
+               the current time. To remove the date from a version, specify
+               an empty string ("").
+               """ % hints,
                self._complete_name, self._do_time)
         yield ('version remove', '<name>',
                "Remove version",
@@ -559,15 +573,18 @@ class VersionAdminPanel(TicketAdminPanel
 
     def _do_list(self):
         print_table([(v.name,
-                      v.time and format_date(v.time, console_date_format))
-                     for v in model.Version.select(self.env)],
+                      format_date(v.time, console_date_format)
+                      if v.time else None)
+                    for v in model.Version.select(self.env)],
                     [_("Name"), _("Time")])
 
     def _do_add(self, name, time=None):
         version = model.Version(self.env)
         version.name = name
         if time is not None:
-            version.time = time and parse_date(time, hint='datetime')
+            version.time = parse_date(time, hint='datetime',
+                                      locale=get_console_locale(self.env)) \
+                           if time else None
         version.insert()
 
     def _do_rename(self, name, newname):
@@ -577,7 +594,9 @@ class VersionAdminPanel(TicketAdminPanel
 
     def _do_time(self, name, time):
         version = model.Version(self.env, name)
-        version.time = time and parse_date(time, hint='datetime')
+        version.time = parse_date(time, hint='datetime',
+                                  locale=get_console_locale(self.env)) \
+                       if time else None
         version.update()
 
     def _do_remove(self, name):

Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/api.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/api.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/api.py 
(original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/api.py Sat Nov 
15 01:14:46 2014
@@ -123,6 +123,15 @@ class ITicketChangeListener(Interface):
     def ticket_deleted(ticket):
         """Called when a ticket is deleted."""
 
+    def ticket_comment_modified(ticket, cdate, author, comment, old_comment):
+        """Called when a ticket comment is modified."""
+
+    def ticket_change_deleted(ticket, cdate, changes):
+        """Called when a ticket change is deleted.
+
+        `changes` is a dictionary of tuple `(oldvalue, newvalue)`
+        containing the ticket change of the fields that have changed."""
+
 
 class ITicketManipulator(Interface):
     """Miscellaneous manipulation of ticket workflow features."""
@@ -562,24 +571,44 @@ class TicketSystem(Component):
                 cnum, realm, id = elts
                 if cnum != 'description' and cnum and not cnum[0].isdigit():
                     realm, id, cnum = elts # support old comment: style
+                id = as_int(id, None)
                 resource = formatter.resource(realm, id)
         else:
             resource = formatter.resource
             cnum = target
 
-        if resource and resource.realm == 'ticket':
-            id = as_int(resource.id, None)
-            if id is not None:
-                href = "%s#comment:%s" % (formatter.href.ticket(resource.id),
-                                          cnum)
-                title = _("Comment %(cnum)s for Ticket #%(id)s", cnum=cnum,
-                          id=resource.id)
-                if 'TICKET_VIEW' in formatter.perm(resource):
-                    for status, in self.env.db_query(
-                            "SELECT status FROM ticket WHERE id=%s", (id,)):
-                        return tag.a(label, href=href, title=title,
-                                     class_=status)
-                return tag.a(label, href=href, title=title)
+        if resource and resource.id and resource.realm == 'ticket' and \
+                cnum and (all(c.isdigit() for c in cnum) or cnum == 
'description'):
+            href = title = class_ = None
+            if self.resource_exists(resource):
+                from trac.ticket.model import Ticket
+                ticket = Ticket(self.env, resource.id)
+                if cnum != 'description' and not ticket.get_change(cnum):
+                    title = _("ticket comment does not exist")
+                    class_ = 'missing ticket'
+                elif 'TICKET_VIEW' in formatter.perm(resource):
+                    href = formatter.href.ticket(resource.id) + \
+                           "#comment:%s" % cnum
+                    if resource.id != formatter.resource.id:
+                        if cnum == 'description':
+                            title = _("Description for Ticket #%(id)s",
+                                      id=resource.id)
+                        else:
+                            title = _("Comment %(cnum)s for Ticket #%(id)s",
+                                      cnum=cnum, id=resource.id)
+                        class_ = ticket['status'] + ' ticket'
+                    else:
+                        title = _("Description") if cnum == 'description' \
+                                                 else _("Comment %(cnum)s",
+                                                        cnum=cnum)
+                        class_ = 'ticket'
+                else:
+                    title = _("no permission to view ticket")
+                    class_ = 'forbidden ticket'
+            else:
+                title = _("ticket does not exist")
+                class_ = 'missing ticket'
+            return tag.a(label, class_=class_, href=href, title=title)
         return label
 
     # IResourceManager methods

Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/batch.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/batch.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/batch.py 
(original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/batch.py Sat 
Nov 15 01:14:46 2014
@@ -29,6 +29,7 @@ from trac.util.translation import _, tag
 from trac.web import IRequestHandler
 from trac.web.chrome import add_warning, add_script_data
 
+
 class BatchModifyModule(Component):
     """Ticket batch modification module.
 

Modified: 
bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/default_workflow.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/default_workflow.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- 
bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/default_workflow.py 
(original)
+++ 
bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/default_workflow.py 
Sat Nov 15 01:14:46 2014
@@ -20,6 +20,7 @@ import pkg_resources
 
 from ConfigParser import RawConfigParser
 from StringIO import StringIO
+from functools import partial
 
 from genshi.builder import tag
 
@@ -98,10 +99,12 @@ class ConfigurableTicketWorkflow(Compone
     """Ticket action controller which provides actions according to a
     workflow defined in trac.ini.
 
-    The workflow is idefined in the `[ticket-workflow]` section of the
+    The workflow is defined in the `[ticket-workflow]` section of the
     [wiki:TracIni#ticket-workflow-section trac.ini] configuration file.
     """
 
+    implements(IEnvironmentSetupParticipant, ITicketActionController)
+
     ticket_workflow_section = ConfigSection('ticket-workflow',
         """The workflow for tickets is controlled by plugins. By default,
         there's only a `ConfigurableTicketWorkflow` component in charge.
@@ -129,7 +132,6 @@ class ConfigurableTicketWorkflow(Compone
                 self.log.warning("Ticket workflow action '%s' doesn't define "
                                  "any transitions", name)
 
-    implements(ITicketActionController, IEnvironmentSetupParticipant)
 
     # IEnvironmentSetupParticipant methods
 
@@ -226,18 +228,14 @@ Read TracWorkflow for more information (
         this_action = self.actions[action]
         status = this_action['newstate']
         operations = this_action['operations']
-        current_owner = ticket._old.get('owner', ticket['owner'] or '(none)')
-        if not (Chrome(self.env).show_email_addresses
-                or 'EMAIL_VIEW' in req.perm(ticket.resource)):
-            format_user = obfuscate_email_address
-        else:
-            format_user = lambda address: address
-        current_owner = format_user(current_owner)
+        current_owner = ticket._old.get('owner', ticket['owner'])
+        format_author = partial(Chrome(self.env).format_author, req)
+        formatted_current_owner = format_author(current_owner or _("(none)"))
 
         control = [] # default to nothing
         hints = []
         if 'reset_workflow' in operations:
-            control.append(tag("from invalid state "))
+            control.append(_("from invalid state"))
             hints.append(_("Current state no longer exists"))
         if 'del_owner' in operations:
             hints.append(_("The ticket will be disowned"))
@@ -245,7 +243,7 @@ Read TracWorkflow for more information (
             id = 'action_%s_reassign_owner' % action
             selected_owner = req.args.get(id, req.authname)
 
-            if this_action.has_key('set_owner'):
+            if 'set_owner' in this_action:
                 owners = [x.strip() for x in
                           this_action['set_owner'].split(',')]
             elif self.config.getbool('ticket', 'restrict_owner'):
@@ -255,41 +253,42 @@ Read TracWorkflow for more information (
             else:
                 owners = None
 
-            if owners == None:
+            if owners is None:
                 owner = req.args.get(id, req.authname)
-                control.append(tag_('to %(owner)s',
+                control.append(tag_("to %(owner)s",
                                     owner=tag.input(type='text', id=id,
                                                     name=id, value=owner)))
                 hints.append(_("The owner will be changed from "
-                               "%(current_owner)s",
-                               current_owner=current_owner))
+                               "%(current_owner)s to the specified user",
+                               current_owner=formatted_current_owner))
             elif len(owners) == 1:
                 owner = tag.input(type='hidden', id=id, name=id,
                                   value=owners[0])
-                formatted_owner = format_user(owners[0])
-                control.append(tag_('to %(owner)s ',
-                                    owner=tag(formatted_owner, owner)))
+                formatted_new_owner = format_author(owners[0])
+                control.append(tag_("to %(owner)s",
+                                    owner=tag(formatted_new_owner, owner)))
                 if ticket['owner'] != owners[0]:
                     hints.append(_("The owner will be changed from "
                                    "%(current_owner)s to %(selected_owner)s",
-                                   current_owner=current_owner,
-                                   selected_owner=formatted_owner))
+                                   current_owner=formatted_current_owner,
+                                   selected_owner=formatted_new_owner))
             else:
-                control.append(tag_('to %(owner)s', owner=tag.select(
+                control.append(tag_("to %(owner)s", owner=tag.select(
                     [tag.option(x, value=x,
                                 selected=(x == selected_owner or None))
                      for x in owners],
                     id=id, name=id)))
                 hints.append(_("The owner will be changed from "
                                "%(current_owner)s to the selected user",
-                               current_owner=current_owner))
+                               current_owner=formatted_current_owner))
         elif 'set_owner_to_self' in operations and \
                 ticket._old.get('owner', ticket['owner']) != req.authname:
             hints.append(_("The owner will be changed from %(current_owner)s "
-                           "to %(authname)s", current_owner=current_owner,
-                           authname=req.authname))
+                           "to %(authname)s",
+                           current_owner=formatted_current_owner,
+                           authname=format_author(req.authname)))
         if 'set_resolution' in operations:
-            if this_action.has_key('set_resolution'):
+            if 'set_resolution' in this_action:
                 resolutions = [x.strip() for x in
                                this_action['set_resolution'].split(',')]
             else:
@@ -302,7 +301,7 @@ Read TracWorkflow for more information (
             if len(resolutions) == 1:
                 resolution = tag.input(type='hidden', id=id, name=id,
                                        value=resolutions[0])
-                control.append(tag_('as %(resolution)s',
+                control.append(tag_("as %(resolution)s",
                                     resolution=tag(resolutions[0],
                                                    resolution)))
                 hints.append(_("The resolution will be set to %(name)s",
@@ -310,7 +309,7 @@ Read TracWorkflow for more information (
             else:
                 selected_option = req.args.get(id,
                         TicketSystem(self.env).default_resolution)
-                control.append(tag_('as %(resolution)s',
+                control.append(tag_("as %(resolution)s",
                                     resolution=tag.select(
                     [tag.option(x, value=x,
                                 selected=(x == selected_option or None))
@@ -320,13 +319,20 @@ Read TracWorkflow for more information (
         if 'del_resolution' in operations:
             hints.append(_("The resolution will be deleted"))
         if 'leave_status' in operations:
-            control.append(_('as %(status)s ',
+            control.append(_("as %(status)s",
                              status= ticket._old.get('status',
                                                      ticket['status'])))
+            if len(operations) == 1:
+                hints.append(_("The owner will remain %(current_owner)s",
+                               current_owner=formatted_current_owner)
+                             if current_owner else
+                             _("The ticket will remain with no owner"))
         else:
             if status != '*':
                 hints.append(_("Next status will be '%(name)s'", name=status))
-        return (this_action['name'], tag(*control), '. '.join(hints) + ".")
+        return (this_action['name'],
+                tag((' ' if i else None, c) for i, c in enumerate(control)),
+                '. '.join(hints) + '.' if hints else '')
 
     def get_ticket_changes(self, req, ticket, action):
         this_action = self.actions[action]

Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/model.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/model.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/model.py 
(original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/model.py Sat 
Nov 15 01:14:46 2014
@@ -524,6 +524,12 @@ class Ticket(object):
 
         self._fetch_ticket(self.id)
 
+        changes = dict((field, (oldvalue, newvalue))
+                       for field, oldvalue, newvalue in fields)
+        for listener in TicketSystem(self.env).change_listeners:
+            if hasattr(listener, 'ticket_change_deleted'):
+                listener.ticket_change_deleted(self, cdate, changes)
+
     def modify_comment(self, cdate, author, comment, when=None):
         """Modify a ticket comment specified by its date, while keeping a
         history of edits.
@@ -548,8 +554,8 @@ class Ticket(object):
             # Find the next edit number
             fields = db("""SELECT field FROM ticket_change
                            WHERE ticket=%%s AND time=%%s AND field %s
-                           """ % db.like(),
-                           (self.id, ts, db.like_escape('_comment') + '%'))
+                           """ % db.prefix_match(),
+                           (self.id, ts, db.prefix_match_value('_comment')))
             rev = max(int(field[8:]) for field, in fields) + 1 if fields else 0
             db("""INSERT INTO ticket_change
                     (ticket,time,author,field,oldvalue,newvalue)
@@ -563,8 +569,8 @@ class Ticket(object):
                 for old_author, in db("""
                         SELECT author FROM ticket_change
                         WHERE ticket=%%s AND time=%%s AND NOT field %s LIMIT 1
-                        """ % db.like(),
-                        (self.id, ts, db.like_escape('_') + '%')):
+                        """ % db.prefix_match(),
+                        (self.id, ts, db.prefix_match_value('_'))):
                     db("""INSERT INTO ticket_change
                             (ticket,time,author,field,oldvalue,newvalue)
                           VALUES (%s,%s,%s,'comment','',%s)
@@ -580,6 +586,12 @@ class Ticket(object):
 
         self.values['changetime'] = when
 
+        old_comment = old_comment or ''
+        for listener in TicketSystem(self.env).change_listeners:
+            if hasattr(listener, 'ticket_comment_modified'):
+                listener.ticket_comment_modified(self, cdate, author, comment,
+                                                 old_comment)
+
     def get_comment_history(self, cnum=None, cdate=None, db=None):
         """Retrieve the edit history of a comment identified by its number or
         date.
@@ -607,8 +619,8 @@ class Ticket(object):
                 for author0, last_comment in db("""
                         SELECT author, newvalue FROM ticket_change
                         WHERE ticket=%%s AND time=%%s AND NOT field %s LIMIT 1
-                        """ % db.like(),
-                        (self.id, ts0, db.like_escape('_') + '%')):
+                        """ % db.prefix_match(),
+                        (self.id, ts0, db.prefix_match_value('_'))):
                     break
                 else:
                     return
@@ -617,8 +629,8 @@ class Ticket(object):
             rows = db("""SELECT field, author, oldvalue, newvalue
                          FROM ticket_change
                          WHERE ticket=%%s AND time=%%s AND field %s
-                         """ % db.like(),
-                         (self.id, ts0, db.like_escape('_comment') + '%'))
+                         """ % db.prefix_match(),
+                         (self.id, ts0, db.prefix_match_value('_comment')))
             rows = sorted((int(field[8:]), author, old, new)
                           for field, author, old, new in rows)
             history = []
@@ -670,8 +682,8 @@ class Ticket(object):
                 for author, in db("""
                         SELECT author FROM ticket_change
                         WHERE ticket=%%s AND time=%%s AND NOT field %s LIMIT 1
-                        """ % db.like(),
-                        (self.id, ts, db.like_escape('_') + '%')):
+                        """ % db.prefix_match(),
+                        (self.id, ts, db.prefix_match_value('_'))):
                     break
             return (ts, author, comment)
 
@@ -1040,23 +1052,20 @@ class Milestone(object):
     def delete(self, retarget_to=None, author=None, db=None):
         """Delete the milestone.
 
+        :since 1.0.2: the `retarget_to` and `author` parameters are
+                      deprecated and will be removed in Trac 1.3.1. Tickets
+                      should be moved to another milestone by calling
+                      `move_tickets` before `delete`.
+
         :since 1.0: the `db` parameter is no longer needed and will be removed
         in version 1.1.1
         """
         with self.env.db_transaction as db:
             self.env.log.info("Deleting milestone %s", self.name)
             db("DELETE FROM milestone WHERE name=%s", (self.name,))
-
-            # Retarget/reset tickets associated with this milestone
-            now = datetime.now(utc)
-            tkt_ids = [int(row[0]) for row in
-                       db("SELECT id FROM ticket WHERE milestone=%s",
-                          (self.name,))]
-            for tkt_id in tkt_ids:
-                ticket = Ticket(self.env, tkt_id, db)
-                ticket['milestone'] = retarget_to
-                comment = "Milestone %s deleted" % self.name # don't translate
-                ticket.save_changes(author, comment, now)
+            Attachment.delete_all(self.env, 'milestone', self.name)
+            # Don't translate ticket comment (comment:40:ticket:5658)
+            self.move_tickets(retarget_to, author, "Milestone deleted")
             self._old['name'] = None
             del self.cache.milestones
             TicketSystem(self.env).reset_ticket_fields()
@@ -1088,7 +1097,7 @@ class Milestone(object):
             listener.milestone_created(self)
         ResourceSystem(self.env).resource_created(self)
 
-    def update(self, db=None):
+    def update(self, db=None, author=None):
         """Update the milestone.
 
         :since 1.0: the `db` parameter is no longer needed and will be removed
@@ -1100,34 +1109,66 @@ class Milestone(object):
 
         old = self._old.copy()
         with self.env.db_transaction as db:
-            old_name = old['name']
-            self.env.log.info("Updating milestone '%s'", self.name)
+            if self.name != old['name']:
+                # Update milestone field in tickets
+                self.move_tickets(self.name, author, "Milestone renamed")
+                TicketSystem(self.env).reset_ticket_fields()
+                # Reparent attachments
+                Attachment.reparent_all(self.env, 'milestone', old['name'],
+                                        'milestone', self.name)
+
+            self.env.log.info("Updating milestone '%s'", old['name'])
             db("""UPDATE milestone
                   SET name=%s, due=%s, completed=%s, description=%s
                   WHERE name=%s
                   """, (self.name, to_utimestamp(self.due),
                         to_utimestamp(self.completed),
-                        self.description, old_name))
+                        self.description, old['name']))
             self.checkin()
 
-            if self.name != old_name:
-                # Update milestone field in tickets
-                self.env.log.info("Updating milestone field of all tickets "
-                                  "associated with milestone '%s'", self.name)
-                db("UPDATE ticket SET milestone=%s WHERE milestone=%s",
-                   (self.name, old_name))
-                TicketSystem(self.env).reset_ticket_fields()
-
-                # Reparent attachments
-                Attachment.reparent_all(self.env, 'milestone', old_name,
-                                        'milestone', self.name)
-
         old_values = dict((k, v) for k, v in old.iteritems()
                           if getattr(self, k) != v)
         for listener in TicketSystem(self.env).milestone_change_listeners:
             listener.milestone_changed(self, old_values)
         ResourceSystem(self.env).resource_changed(self, old_values)
 
+    def move_tickets(self, new_milestone, author, comment=None,
+                     exclude_closed=False):
+        """Move tickets associated with this milestone to another
+        milestone.
+
+        :param new_milestone: milestone to which the tickets are moved
+        :param author: author of the change
+        :param comment: comment that is inserted into moved tickets. The
+                        string should not be translated.
+        :param exclude_closed: whether tickets with status closed should be
+                               excluded
+
+        :return: a list of ids of tickets that were moved
+        """
+        # Check if milestone exists, but if the milestone is being renamed
+        # the new milestone won't exist in the cache yet so skip the test
+        if new_milestone and new_milestone != self.name:
+            if not self.cache.fetchone(new_milestone):
+                raise ResourceNotFound(
+                    _("Milestone %(name)s does not exist.",
+                      name=new_milestone), _("Invalid milestone name"))
+        now = datetime.now(utc)
+        with self.env.db_transaction as db:
+            sql = "SELECT id FROM ticket WHERE milestone=%s"
+            if exclude_closed:
+                sql += " AND status != 'closed'"
+            tkt_ids = [int(row[0]) for row in db(sql, (self._old['name'],))]
+            if tkt_ids:
+                self.env.log.info("Moving tickets associated with milestone "
+                                  "'%s' to milestone '%s'", self._old['name'],
+                                  new_milestone)
+                for tkt_id in tkt_ids:
+                    ticket = Ticket(self.env, tkt_id)
+                    ticket['milestone'] = new_milestone
+                    ticket.save_changes(author, comment, now)
+        return tkt_ids
+
     @classmethod
     def select(cls, env, include_completed=True, db=None):
         """

Modified: 
bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/notification.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/notification.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/notification.py 
(original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/notification.py 
Sat Nov 15 01:14:46 2014
@@ -26,8 +26,10 @@ from trac.core import *
 from trac.config import *
 from trac.notification import NotifyEmail
 from trac.ticket.api import TicketSystem
+from trac.ticket.model import Ticket
 from trac.util.datefmt import to_utimestamp
-from trac.util.text import obfuscate_email_address, text_width, wrap
+from trac.util.text import obfuscate_email_address, shorten_line, \
+                           text_width, wrap
 from trac.util.translation import deactivate, reactivate
 
 
@@ -57,7 +59,7 @@ class TicketNotificationSystem(Component
         ''(since 0.11)''""")
 
     batch_subject_template = Option('notification', 'batch_subject_template',
-                                     '$prefix Batch modify: $tickets_descr',
+                                    '$prefix Batch modify: $tickets_descr',
         """Like ticket_subject_template but for batch modifications.
 
         By default, the template is `$prefix Batch modify: $tickets_descr`.
@@ -73,60 +75,70 @@ class TicketNotificationSystem(Component
         US-ASCII characters.  This is expected by CJK users. ''(since
         0.12.2)''""")
 
-def get_ticket_notification_recipients(env, config, tktid, prev_cc):
-    notify_reporter = config.getbool('notification', 'always_notify_reporter')
-    notify_owner = config.getbool('notification', 'always_notify_owner')
-    notify_updater = config.getbool('notification', 'always_notify_updater')
-
-    ccrecipients = prev_cc
-    torecipients = []
-    with env.db_query as db:
-        # Harvest email addresses from the cc, reporter, and owner fields
-        for row in db("SELECT cc, reporter, owner FROM ticket WHERE id=%s",
-                      (tktid,)):
-            if row[0]:
-                ccrecipients += row[0].replace(',', ' ').split()
-            reporter = row[1]
-            owner = row[2]
-            if notify_reporter:
-                torecipients.append(row[1])
-            if notify_owner:
-                torecipients.append(row[2])
-            break
-
-        # Harvest email addresses from the author field of ticket_change(s)
-        if notify_updater:
-            for author, ticket in db("""
-                    SELECT DISTINCT author, ticket FROM ticket_change
-                    WHERE ticket=%s
-                    """, (tktid,)):
-                torecipients.append(author)
-
-        # Suppress the updater from the recipients
-        updater = None
-        for updater, in db("""
-                SELECT author FROM ticket_change WHERE ticket=%s
-                ORDER BY time DESC LIMIT 1
-                """, (tktid,)):
-            break
-        else:
-            for updater, in db("SELECT reporter FROM ticket WHERE id=%s",
-                               (tktid,)):
-                break
-
-        if not notify_updater:
-            filter_out = True
-            if notify_reporter and (updater == reporter):
-                filter_out = False
-            if notify_owner and (updater == owner):
-                filter_out = False
-            if filter_out:
-                torecipients = [r for r in torecipients
-                                if r and r != updater]
-        elif updater:
-            torecipients.append(updater)
 
-    return (torecipients, ccrecipients, reporter, owner)
+def get_ticket_notification_recipients(env, config, tktid, prev_cc=None,
+                                       modtime=None):
+    """Returns notifications recipients.
+
+    :since 1.0.2: the `config` parameter is no longer used.
+    :since 1.0.2: the `prev_cc` parameter is deprecated.
+    """
+    section = env.config['notification']
+    always_notify_reporter = section.getbool('always_notify_reporter')
+    always_notify_owner = section.getbool('always_notify_owner')
+    always_notify_updater = section.getbool('always_notify_updater')
+
+    cc_recipients = set(prev_cc or [])
+    to_recipients = set()
+    tkt = Ticket(env, tktid)
+
+    # CC field is stored as comma-separated string. Parse to list.
+    to_list = lambda cc: cc.replace(',', ' ').split()
+
+    # Backward compatibility
+    if not modtime:
+        modtime = tkt['changetime']
+
+    # Harvest email addresses from the cc, reporter, and owner fields
+    if tkt['cc']:
+        cc_recipients.update(to_list(tkt['cc']))
+    if always_notify_reporter:
+        to_recipients.add(tkt['reporter'])
+    if always_notify_owner:
+        to_recipients.add(tkt['owner'])
+
+    # Harvest email addresses from the author field of ticket_change(s)
+    if always_notify_updater:
+        for author, ticket in env.db_query("""
+                SELECT DISTINCT author, ticket FROM ticket_change
+                WHERE ticket=%s
+                """, (tktid, )):
+            to_recipients.add(author)
+
+    # Harvest previous owner and cc list
+    author = None
+    for changelog in tkt.get_changelog(modtime):
+        author, field, old = changelog[1:4]
+        if field == 'owner' and always_notify_owner:
+            to_recipients.add(old)
+        elif field == 'cc':
+            cc_recipients.update(to_list(old))
+
+    # Suppress the updater from the recipients if necessary
+    updater = author or tkt['reporter']
+    if not always_notify_updater:
+        filter_out = True
+        if always_notify_reporter and updater == tkt['reporter']:
+            filter_out = False
+        if always_notify_owner and updater == tkt['owner']:
+            filter_out = False
+        if filter_out:
+            to_recipients.discard(updater)
+    elif updater:
+        to_recipients.add(updater)
+
+    return list(to_recipients), list(cc_recipients), \
+           tkt['reporter'], tkt['owner']
 
 
 class TicketNotifyEmail(NotifyEmail):
@@ -141,7 +153,6 @@ class TicketNotifyEmail(NotifyEmail):
 
     def __init__(self, env):
         NotifyEmail.__init__(self, env)
-        self.prev_cc = []
         ambiguous_char_width = env.config.get('notification',
                                               'ambiguous_char_width',
                                               'single')
@@ -219,7 +230,6 @@ class TicketNotifyEmail(NotifyEmail):
                                           self.ambiwidth) + '\n'
                         if chgcc:
                             changes_body += chgcc
-                        self.prev_cc += self.parse_cc(old) if old else []
                     else:
                         if field in ['owner', 'reporter']:
                             old = self.obfuscate_email(old)
@@ -306,13 +316,13 @@ class TicketNotifyEmail(NotifyEmail):
                 width_l = self.COLS - width_r - 1
         sep = width_l * '-' + '+' + width_r * '-'
         txt = sep + '\n'
-        cell_tmp = [u'', u'']
+        vals_lr = ([], [])
         big = []
         i = 0
         width_lr = [width_l, width_r]
         for f in [f for f in fields if f['name'] != 'description']:
             fname = f['name']
-            if not tkt.values.has_key(fname):
+            if fname not in tkt.values:
                 continue
             fval = tkt[fname] or ''
             if fname in ['owner', 'reporter']:
@@ -324,15 +334,36 @@ class TicketNotifyEmail(NotifyEmail):
                 # __str__ method won't be called.
                 str_tmp = u'%s:  %s' % (f['label'], unicode(fval))
                 idx = i % 2
-                cell_tmp[idx] += wrap(str_tmp, width_lr[idx] - 2 + 2 * idx,
-                                      (width[2 * idx]
-                                       - self.get_text_width(f['label'])
-                                       + 2 * idx) * ' ',
-                                      2 * ' ', '\n', self.ambiwidth)
-                cell_tmp[idx] += '\n'
+                initial_indent = ' ' * (width[2 * idx] -
+                                        self.get_text_width(f['label']) +
+                                        2 * idx)
+                wrapped = wrap(str_tmp, width_lr[idx] - 2 + 2 * idx,
+                               initial_indent, '  ', '\n', self.ambiwidth)
+                vals_lr[idx].append(wrapped.splitlines())
                 i += 1
-        cell_l = cell_tmp[0].splitlines()
-        cell_r = cell_tmp[1].splitlines()
+        if len(vals_lr[0]) > len(vals_lr[1]):
+            vals_lr[1].append([])
+
+        cell_l = []
+        cell_r = []
+        for i in xrange(len(vals_lr[0])):
+            vals_l = vals_lr[0][i]
+            vals_r = vals_lr[1][i]
+            vals_diff = len(vals_l) - len(vals_r)
+            diff = len(cell_l) - len(cell_r)
+            if diff > 0:
+                # add padding to right side if needed
+                if vals_diff < 0:
+                    diff += vals_diff
+                cell_r.extend([''] * max(diff, 0))
+            elif diff < 0:
+                # add padding to left side if needed
+                if vals_diff > 0:
+                    diff += vals_diff
+                cell_l.extend([''] * max(-diff, 0))
+            cell_l.extend(vals_l)
+            cell_r.extend(vals_r)
+
         for i in range(max(len(cell_l), len(cell_r))):
             if i >= len(cell_l):
                 cell_l.append(width_l * ' ')
@@ -383,9 +414,9 @@ class TicketNotifyEmail(NotifyEmail):
         return template.generate(**data).render('text', encoding=None).strip()
 
     def get_recipients(self, tktid):
-        (torecipients, ccrecipients, reporter, owner) = \
-            get_ticket_notification_recipients(self.env, self.config,
-                tktid, self.prev_cc)
+        torecipients, ccrecipients, reporter, owner = \
+            get_ticket_notification_recipients(self.env, self.config, tktid,
+                                               modtime=self.modtime)
         self.reporter = reporter
         self.owner = owner
         return (torecipients, ccrecipients)
@@ -425,6 +456,7 @@ class TicketNotifyEmail(NotifyEmail):
         else:
             return obfuscate_email_address(text)
 
+
 class BatchTicketNotifyEmail(NotifyEmail):
     """Notification of ticket batch modifications."""
 
@@ -443,7 +475,6 @@ class BatchTicketNotifyEmail(NotifyEmail
 
     def _notify(self, tickets, new_values, comment, action, author):
         self.tickets = tickets
-        changes_body = ''
         self.reporter = ''
         self.owner = ''
         changes_descr = '\n'.join(['%s to %s' % (prop, val)
@@ -475,16 +506,15 @@ class BatchTicketNotifyEmail(NotifyEmail
             'tickets_descr': tickets_descr,
             'env': self.env,
         }
-
-        return template.generate(**data).render('text', encoding=None).strip()
+        subj = template.generate(**data).render('text', encoding=None).strip()
+        return shorten_line(subj)
 
     def get_recipients(self, tktids):
-        alltorecipients = []
-        allccrecipients = []
+        alltorecipients = set()
+        allccrecipients = set()
         for t in tktids:
-            (torecipients, ccrecipients, reporter, owner) = \
-                get_ticket_notification_recipients(self.env, self.config,
-                    t, [])
-            alltorecipients.extend(torecipients)
-            allccrecipients.extend(ccrecipients)
-        return (list(set(alltorecipients)), list(set(allccrecipients)))
+            torecipients, ccrecipients, reporter, owner = \
+                get_ticket_notification_recipients(self.env, self.config, t)
+            alltorecipients.update(torecipients)
+            allccrecipients.update(ccrecipients)
+        return list(alltorecipients), list(allccrecipients)

Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/query.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/query.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/query.py 
(original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/query.py Sat 
Nov 15 01:14:46 2014
@@ -32,14 +32,15 @@ from trac.db import get_column_names
 from trac.mimeview.api import IContentConverter, Mimeview
 from trac.resource import Resource
 from trac.ticket.api import TicketSystem
-from trac.ticket.model import Milestone, group_milestones, Ticket
+from trac.ticket.model import Milestone, group_milestones
 from trac.util import Ranges, as_bool
+from trac.util.compat import any
 from trac.util.datefmt import format_date, format_datetime, from_utimestamp, \
                               parse_date, to_timestamp, to_utimestamp, utc, \
                               user_time
 from trac.util.presentation import Paginator
 from trac.util.text import empty, shorten_line, quote_query_string
-from trac.util.translation import _, tag_, cleandoc_
+from trac.util.translation import _, tag_, cleandoc_, ngettext
 from trac.web import arg_list_to_args, parse_arg_list, IRequestHandler
 from trac.web.href import Href
 from trac.web.chrome import (INavigationContributor, Chrome,
@@ -434,11 +435,12 @@ class Query(object):
 
         enum_columns = ('resolution', 'priority', 'severity')
         # Build the list of actual columns to query
-        cols = self.cols[:]
+        cols = []
         def add_cols(*args):
             for col in args:
                 if not col in cols:
                     cols.append(col)
+        add_cols(*self.cols)  # remove duplicated cols
         if self.group and not self.group in cols:
             add_cols(self.group)
         if self.rows:
@@ -456,14 +458,20 @@ class Query(object):
                                          if c not in custom_fields]))
         sql.append(",priority.value AS priority_value")
         for k in [db.quote(k) for k in cols if k in custom_fields]:
-            sql.append(",%s.value AS %s" % (k, k))
-        sql.append("\nFROM ticket AS t")
+            sql.append(",t.%s AS %s" % (k, k))
 
-        # Join with ticket_custom table as necessary
-        for k in [k for k in cols if k in custom_fields]:
-            qk = db.quote(k)
-            sql.append("\n  LEFT OUTER JOIN ticket_custom AS %s ON " \
-                       "(id=%s.ticket AND %s.name='%s')" % (qk, qk, qk, k))
+        # Use subquery of ticket_custom table as necessary
+        if any(k in custom_fields for k in cols):
+            sql.append('\nFROM (\n  SELECT ' +
+                       ','.join('t.%s AS %s' % (c, c)
+                                for c in cols if c not in custom_fields))
+            sql.extend(",\n  (SELECT c.value FROM ticket_custom c "
+                       "WHERE c.ticket=t.id AND c.name='%s') AS %s"
+                       % (k, db.quote(k))
+                       for k in cols if k in custom_fields)
+            sql.append("\n  FROM ticket AS t) AS t")
+        else:
+            sql.append("\nFROM ticket AS t")
 
         # Join with the enum table for proper sorting
         for col in [c for c in enum_columns
@@ -490,7 +498,7 @@ class Query(object):
             if name not in custom_fields:
                 col = 't.' + name
             else:
-                col = '%s.value' % db.quote(name)
+                col = 't.' + db.quote(name)
             value = value[len(mode) + neg:]
 
             if name in self.time_fields:
@@ -579,11 +587,11 @@ class Query(object):
                         if a == b:
                             ids.append(str(a))
                         else:
-                            id_clauses.append('id BETWEEN %s AND %s')
+                            id_clauses.append('t.id BETWEEN %s AND %s')
                             args.append(a)
                             args.append(b)
                     if ids:
-                        id_clauses.append('id IN (%s)' % (','.join(ids)))
+                        id_clauses.append('t.id IN (%s)' % (','.join(ids)))
                     if id_clauses:
                         clauses.append('%s(%s)' % ('NOT 'if neg else '',
                                                    ' OR '.join(id_clauses)))
@@ -592,7 +600,7 @@ class Query(object):
                     if k not in custom_fields:
                         col = 't.' + k
                     else:
-                        col = '%s.value' % db.quote(k)
+                        col = 't.' + db.quote(k)
                     clauses.append("COALESCE(%s,'') %sIN (%s)"
                                    % (col, 'NOT ' if neg else '',
                                       ','.join(['%s' for val in v])))
@@ -633,7 +641,7 @@ class Query(object):
             if name in enum_columns:
                 col = name + '.value'
             elif name in custom_fields:
-                col = '%s.value' % db.quote(name)
+                col = 't.' + db.quote(name)
             else:
                 col = 't.' + name
             desc = ' DESC' if desc else ''
@@ -716,11 +724,17 @@ class Query(object):
         cols = self.get_columns()
         labels = TicketSystem(self.env).get_ticket_field_labels()
         wikify = set(f['name'] for f in self.fields
-                     if f['type'] == 'text' and f.get('format') == 'wiki')
+                     if f['type'] == 'text' and
+                        f.get('format') == 'wiki')
+        wikifyblock = set(f['name'] for f in self.fields
+                          if f['type'] == 'textarea' and
+                             f.get('format') == 'wiki')
+        wikifyblock.add('description')
 
         headers = [{
             'name': col, 'label': labels.get(col, _('Ticket')),
             'wikify': col in wikify,
+            'wikifyblock': col in wikifyblock,
             'href': self.get_href(context.href, order=col,
                                   desc=(col == self.order and not self.desc))
         } for col in cols]
@@ -872,7 +886,8 @@ class QueryModule(Component):
     def get_navigation_items(self, req):
         from trac.ticket.report import ReportModule
         if 'TICKET_VIEW' in req.perm and \
-                not self.env.is_component_enabled(ReportModule):
+                not (self.env.is_component_enabled(ReportModule) and
+                     'REPORT_VIEW' in req.perm):
             yield ('mainnav', 'tickets',
                    tag.a(_('View Tickets'), href=req.href.query()))
 
@@ -883,6 +898,9 @@ class QueryModule(Component):
 
     def process_request(self, req):
         req.perm.assert_permission('TICKET_VIEW')
+        report_id = req.args.get('report')
+        if report_id:
+            req.perm('report', report_id).assert_permission('REPORT_VIEW')
 
         constraints = self._get_constraints(req)
         args = req.args
@@ -937,7 +955,7 @@ class QueryModule(Component):
         max = args.get('max')
         if max is None and format in ('csv', 'tab'):
             max = 0 # unlimited unless specified explicitly
-        query = Query(self.env, req.args.get('report'),
+        query = Query(self.env, report_id,
                       constraints, cols, args.get('order'),
                       'desc' in args, args.get('group'),
                       'groupdesc' in args, 'verbose' in args,
@@ -1151,7 +1169,7 @@ class QueryModule(Component):
                 values = []
                 for col in cols:
                     value = result[col]
-                    if col in ('cc', 'reporter'):
+                    if col in ('cc', 'owner', 'reporter'):
                         value = Chrome(self.env).format_emails(
                                     context.child(ticket), value)
                     elif col in query.time_fields:
@@ -1215,7 +1233,7 @@ class TicketQueryMacro(WikiMacroBase):
     can be included in field values by escaping them with a backslash (`\`).
 
     Groups of field constraints to be OR-ed together can be separated by a
-    litteral `or` argument.
+    literal `or` argument.
 
     In addition to filters, several other named parameters can be used
     to control how the results are presented. All of them are optional.
@@ -1253,6 +1271,9 @@ class TicketQueryMacro(WikiMacroBase):
     The `rows` parameter can be used to specify which field(s) should
     be viewed as a row, e.g. `rows=description|summary`
 
+    The `col` parameter can be used to specify which fields should
+    be viewed as columns. For '''table''' format only.
+
     For compatibility with Trac 0.10, if there's a last positional parameter
     given to the macro, it will be used to specify the `format`.
     Also, using "&" as a field separator still works (except for `order`)
@@ -1313,8 +1334,10 @@ class TicketQueryMacro(WikiMacroBase):
 
         if format == 'count':
             cnt = query.count(req)
-            return tag.span(cnt, title='%d tickets for which %s' %
-                            (cnt, query_string), class_='query_count')
+            title = ngettext("%(num)d ticket for which %(query)s",
+                             "%(num)d tickets for which %(query)s",
+                             cnt, query=query_string)
+            return tag.span(cnt, title=title, class_='query_count')
 
         tickets = query.execute(req)
 
@@ -1336,14 +1359,21 @@ class TicketQueryMacro(WikiMacroBase):
             add_stylesheet(req, 'common/css/roadmap.css')
 
             def query_href(extra_args, group_value = None):
-                q = Query.from_string(self.env, query_string)
+                q = query_string + ''.join('&%s=%s' % (kw, v)
+                                           for kw in extra_args
+                                           if kw not in ['group', 'status']
+                                           for v in extra_args[kw])
+                q = Query.from_string(self.env, q)
+                args = {}
                 if q.group:
-                    extra_args[q.group] = group_value
-                    q.group = None
+                    args[q.group] = group_value
+                q.group = extra_args.get('group')
+                if 'status' in extra_args:
+                    args['status'] = extra_args['status']
                 for constraint in q.constraints:
-                    constraint.update(extra_args)
+                    constraint.update(args)
                 if not q.constraints:
-                    q.constraints.append(extra_args)
+                    q.constraints.append(args)
                 return q.get_href(formatter.context)
             chrome = Chrome(self.env)
             tickets = apply_ticket_permissions(self.env, req, tickets)

Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/report.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/report.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/report.py 
(original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/report.py Sat 
Nov 15 01:14:46 2014
@@ -43,7 +43,6 @@ from trac.web.chrome import (INavigation
 from trac.wiki import IWikiSyntaxProvider, WikiParser
 
 
-
 SORT_COLUMN = '@SORT_COLUMN@'
 LIMIT_OFFSET = '@LIMIT_OFFSET@'
 
@@ -63,9 +62,11 @@ _sql_re = re.compile(r'''
     | \([^()]+\)                   # parenthesis group
 ''', re.MULTILINE | re.VERBOSE)
 
+
 def _expand_with_space(m):
     return ' ' * len(m.group(0))
 
+
 def sql_skeleton(sql):
     """Strip an SQL query to leave only its toplevel structure.
 
@@ -89,6 +90,7 @@ def sql_skeleton(sql):
 
 _order_by_re = re.compile(r'ORDER\s+BY', re.MULTILINE)
 
+
 def split_sql(sql, clause_re, skel=None):
     """Split an SQL query according to a toplevel clause regexp.
 
@@ -107,7 +109,6 @@ def split_sql(sql, clause_re, skel=None)
         return sql, '' # no single clause separator
 
 
-
 class ReportModule(Component):
 
     implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
@@ -148,13 +149,15 @@ class ReportModule(Component):
             return True
 
     def process_request(self, req):
-        req.perm.require('REPORT_VIEW')
-
         # did the user ask for any special report?
         id = int(req.args.get('id', -1))
-        action = req.args.get('action', 'view')
+        if id != -1:
+            req.perm('report', id).require('REPORT_VIEW')
+        else:
+            req.perm.require('REPORT_VIEW')
 
         data = {}
+        action = req.args.get('action', 'view')
         if req.method == 'POST':
             if action == 'new':
                 self._do_create(req)
@@ -164,7 +167,7 @@ class ReportModule(Component):
                 self._do_save(req, id)
         elif action in ('copy', 'edit', 'new'):
             template = 'report_edit.html'
-            data = self._render_editor(req, id, action=='copy')
+            data = self._render_editor(req, id, action == 'copy')
             Chrome(self.env).add_wiki_toolbars(req)
         elif action == 'delete':
             template = 'report_delete.html'
@@ -183,17 +186,19 @@ class ReportModule(Component):
             if content_type: # i.e. alternate format
                 return template, data, content_type
 
+        from trac.ticket.query import QueryModule
+        show_query_link = 'TICKET_VIEW' in req.perm and \
+                          self.env.is_component_enabled(QueryModule)
+
         if id != -1 or action == 'new':
             add_ctxtnav(req, _('Available Reports'), href=req.href.report())
             add_link(req, 'up', req.href.report(), _('Available Reports'))
-        else:
+        elif show_query_link:
             add_ctxtnav(req, _('Available Reports'))
 
         # Kludge: only show link to custom query if the query module
         # is actually enabled
-        from trac.ticket.query import QueryModule
-        if 'TICKET_VIEW' in req.perm and \
-                self.env.is_component_enabled(QueryModule):
+        if show_query_link:
             add_ctxtnav(req, _('Custom Query'), href=req.href.query())
             data['query_href'] = req.href.query()
             data['saved_query_href'] = req.session.get('query_href')
@@ -224,7 +229,7 @@ class ReportModule(Component):
         req.redirect(req.href.report(report_id))
 
     def _do_delete(self, req, id):
-        req.perm.require('REPORT_DELETE')
+        req.perm('report', id).require('REPORT_DELETE')
 
         if 'cancel' in req.args:
             req.redirect(req.href.report(id))
@@ -235,7 +240,7 @@ class ReportModule(Component):
 
     def _do_save(self, req, id):
         """Save report changes to the database"""
-        req.perm.require('REPORT_MODIFY')
+        req.perm('report', id).require('REPORT_MODIFY')
 
         if 'cancel' not in req.args:
             title = req.args.get('title', '')
@@ -249,29 +254,18 @@ class ReportModule(Component):
         req.redirect(req.href.report(id))
 
     def _render_confirm_delete(self, req, id):
-        req.perm.require('REPORT_DELETE')
+        req.perm('report', id).require('REPORT_DELETE')
 
-        for title, in self.env.db_query("""
-                SELECT title FROM report WHERE id=%s
-                """, (id,)):
-            return {'title': _("Delete Report {%(num)s} %(title)s", num=id,
-                               title=title),
-                    'action': 'delete',
-                    'report': {'id': id, 'title': title}}
-        else:
-            raise TracError(_("Report {%(num)s} does not exist.", num=id),
-                            _("Invalid Report Number"))
+        title = self.get_report(id)[0]
+        return {'title': _("Delete Report {%(num)s} %(title)s", num=id,
+                           title=title),
+                'action': 'delete',
+                'report': {'id': id, 'title': title}}
 
     def _render_editor(self, req, id, copy):
         if id != -1:
-            req.perm.require('REPORT_MODIFY')
-            for title, description, query in self.env.db_query(
-                    "SELECT title, description, query FROM report WHERE id=%s",
-                    (id,)):
-                break
-            else:
-                raise TracError(_("Report {%(num)s} does not exist.", num=id),
-                                _("Invalid Report Number"))
+            req.perm('report', id).require('REPORT_MODIFY')
+            title, description, query = self.get_report(id)
         else:
             req.perm.require('REPORT_CREATE')
             title = description = query = ''
@@ -306,6 +300,8 @@ class ReportModule(Component):
                 SELECT id, title, description FROM report ORDER BY %s %s
                 """ % ('title' if sort == 'title' else 'id',
                        '' if asc else 'DESC'))
+        rows = [(id, title, description) for id, title, description in rows
+                if 'REPORT_VIEW' in req.perm('report', id)]
 
         if format == 'rss':
             data = {'rows': rows}
@@ -344,13 +340,7 @@ class ReportModule(Component):
 
     def _render_view(self, req, id):
         """Retrieve the report results and pre-process them for rendering."""
-        for title, sql, description in self.env.db_query("""
-                SELECT title, query, description from report WHERE id=%s
-                """, (id,)):
-            break
-        else:
-            raise ResourceNotFound(_("Report {%(num)s} does not exist.",
-                                     num=id), _("Invalid Report Number"))
+        title, description, sql = self.get_report(id)
         try:
             args = self.get_var_args(req)
         except ValueError, e:
@@ -393,7 +383,7 @@ class ReportModule(Component):
         title = '{%i} %s' % (id, title)
 
         report_resource = Resource('report', id)
-        req.perm.require('REPORT_VIEW', report_resource)
+        req.perm(report_resource).require('REPORT_VIEW')
         context = web_context(req, report_resource)
 
         page = int(req.args.get('page', '1'))
@@ -436,13 +426,13 @@ class ReportModule(Component):
                                                 offset)
 
         if len(res) == 2:
-             e, sql = res
-             data['message'] = \
-                 tag_("Report execution failed: %(error)s %(sql)s",
-                      error=tag.pre(exception_to_unicode(e)),
-                      sql=tag(tag.hr(),
-                              tag.pre(sql, style="white-space: pre")))
-             return 'report_view.html', data, None
+            e, sql = res
+            data['message'] = \
+                tag_("Report execution failed: %(error)s %(sql)s",
+                     error=tag.pre(exception_to_unicode(e)),
+                     sql=tag(tag.hr(),
+                             tag.pre(sql, style="white-space: pre")))
+            return 'report_view.html', data, None
 
         cols, results, num_items, missing_args, limit_offset = res
         need_paginator = limit > 0 and limit_offset
@@ -469,8 +459,8 @@ class ReportModule(Component):
             fields = ['href', 'class', 'string', 'title']
             paginator.shown_pages = [dict(zip(fields, p)) for p in pagedata]
             paginator.current_page = {'href': None, 'class': 'current',
-                                    'string': str(paginator.page + 1),
-                                    'title': None}
+                                      'string': str(paginator.page + 1),
+                                      'title': None}
             numrows = paginator.num_items
 
         # Place retrieved columns in groups, according to naming conventions
@@ -609,7 +599,7 @@ class ReportModule(Component):
         if format == 'rss':
             data['email_map'] = chrome.get_email_map()
             data['context'] = web_context(req, report_resource,
-                                                   absurls=True)
+                                          absurls=True)
             return 'report.rss', data, 'application/rss+xml'
         elif format == 'csv':
             filename = 'report_%s.csv' % id if id else 'report.csv'
@@ -629,7 +619,7 @@ class ReportModule(Component):
                      _('Comma-delimited Text'), 'text/plain')
             add_link(req, 'alternate', report_href(format='tab', page=p),
                      _('Tab-delimited Text'), 'text/plain')
-            if 'REPORT_SQL_VIEW' in req.perm:
+            if 'REPORT_SQL_VIEW' in req.perm('report', id):
                 add_link(req, 'alternate',
                          req.href.report(id=id, format='sql'),
                          _('SQL Query'), 'text/plain')
@@ -692,6 +682,9 @@ class ReportModule(Component):
             try:
                 cursor.execute(count_sql, args)
             except Exception, e:
+                self.log.warn('Exception caught while executing report: %r, '
+                              'args %r%s', count_sql, args,
+                              exception_to_unicode(e, traceback=True))
                 return e, count_sql
             num_items = cursor.fetchone()[0]
 
@@ -701,6 +694,9 @@ class ReportModule(Component):
             try:
                 cursor.execute(colnames_sql, args)
             except Exception, e:
+                self.log.warn('Exception caught while executing report: %r, '
+                              'args %r%s', colnames_sql, args,
+                              exception_to_unicode(e, traceback=True))
                 return e, colnames_sql
             cols = get_column_names(cursor)
 
@@ -724,7 +720,7 @@ class ReportModule(Component):
                 sql = sql.replace(SORT_COLUMN, sort_col or '1')
             elif sort_col:
                 # Method 2: automagically insert sort_col (and __group__
-                # before it, if __group__ was specified) as first criterions
+                # before it, if __group__ was specified) as first criteria
                 if '__group__' in cols:
                     order_by.append('__group__ ASC')
                 order_by.append(sort_col)
@@ -752,6 +748,9 @@ class ReportModule(Component):
         try:
             cursor.execute(sql, args)
         except Exception, e:
+            self.log.warn('Exception caught while executing report: %r, args '
+                          '%r%s',
+                          sql, args, exception_to_unicode(e, traceback=True))
             if order_by or limit_offset:
                 add_notice(req, _("Hint: if the report failed due to automatic"
                                   " modification of the ORDER BY clause or the"
@@ -766,6 +765,20 @@ class ReportModule(Component):
         cols = get_column_names(cursor)
         return cols, rows, num_items, missing_args, limit_offset
 
+    def get_report(self, id):
+        try:
+            number = int(id)
+        except (ValueError, TypeError):
+            pass
+        else:
+            for title, description, sql in self.env.db_query("""
+                    SELECT title, description, query from report WHERE id=%s
+                    """, (number,)):
+                return title, description, sql
+
+        raise ResourceNotFound(_("Report {%(num)s} does not exist.", num=id),
+                               _("Invalid Report Number"))
+
     def get_var_args(self, req):
         # reuse somehow for #9574 (wiki vars)
         report_args = {}
@@ -874,7 +887,7 @@ class ReportModule(Component):
         raise RequestDone
 
     def _send_sql(self, req, id, title, description, sql):
-        req.perm.require('REPORT_SQL_VIEW')
+        req.perm('report', id).require('REPORT_SQL_VIEW')
 
         out = StringIO()
         out.write('-- ## %s: %s ## --\n\n' % (id, title.encode('utf-8')))
@@ -901,8 +914,8 @@ class ReportModule(Component):
         yield ('report', self._format_link)
 
     def get_wiki_syntax(self):
-        yield (r"!?\{(?P<it_report>%s\s*)[0-9]+\}" % \
-                                                WikiParser.INTERTRAC_SCHEME,
+        yield (r"!?\{(?P<it_report>%s\s*)[0-9]+\}" %
+                   WikiParser.INTERTRAC_SCHEME,
                lambda x, y, z: self._format_link(x, 'report', y[1:-1], y, z))
 
     def _format_link(self, formatter, ns, target, label, fullmatch=None):
@@ -910,6 +923,16 @@ class ReportModule(Component):
                                                          fullmatch)
         if intertrac:
             return intertrac
-        report, args, fragment = formatter.split_link(target)
-        return tag.a(label, href=formatter.href.report(report) + args,
-                     class_='report')
+        id, args, fragment = formatter.split_link(target)
+        try:
+            self.get_report(id)
+        except ResourceNotFound:
+            return tag.a(label, class_='missing report',
+                         title=_("report does not exist"))
+        else:
+            if 'REPORT_VIEW' in formatter.req.perm('report', id):
+                return tag.a(label, href=formatter.href.report(id) + args,
+                             class_='report')
+            else:
+                return tag.a(label, class_='forbidden report',
+                             title=_("no permission to view report"))

Modified: bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/roadmap.py
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/roadmap.py?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/roadmap.py 
(original)
+++ bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/roadmap.py Sat 
Nov 15 01:14:46 2014
@@ -31,12 +31,13 @@ from trac.perm import IPermissionRequest
 from trac.resource import *
 from trac.search import ISearchSource, search_to_regexps, shorten_result
 from trac.util import as_bool
-from trac.util.datefmt import parse_date, utc, to_utimestamp, to_datetime, \
+from trac.util.datefmt import parse_date, utc, pretty_timedelta, to_datetime, \
                               get_datetime_format_hint, format_date, \
                               format_datetime, from_utimestamp, user_time
-from trac.util.text import CRLF
+from trac.util.text import CRLF, exception_to_unicode, to_unicode
 from trac.util.translation import _, tag_
 from trac.ticket.api import TicketSystem
+from trac.ticket.batch import BatchTicketNotifyEmail
 from trac.ticket.model import Milestone, MilestoneCache, Ticket, \
                               group_milestones
 from trac.timeline.api import ITimelineEventProvider
@@ -349,6 +350,13 @@ def grouped_stats_data(env, stats_provid
                 group_names = field['options']
                 if field.get('optional'):
                     group_names.insert(0, '')
+            elif field.get('custom'):
+                group_names = [name for name, in env.db_query("""
+                    SELECT DISTINCT COALESCE(c.value, '') FROM ticket_custom c
+                    WHERE c.name=%s ORDER BY COALESCE(c.value, '')
+                    """, (by, ))]
+                if '' not in group_names:
+                    group_names.insert(0, '')
             else:
                 group_names = [name for name, in env.db_query("""
                     SELECT DISTINCT COALESCE(%s, '') FROM ticket
@@ -415,7 +423,7 @@ class RoadmapModule(Component):
         return req.path_info == '/roadmap'
 
     def process_request(self, req):
-        req.perm.require('MILESTONE_VIEW')
+        req.perm.require('ROADMAP_VIEW')
 
         show = req.args.getlist('show')
         if 'all' in show:
@@ -671,7 +679,7 @@ class MilestoneModule(Component):
             action = 'edit' # rather than 'new' so that it works for POST/save
 
         if req.method == 'POST':
-            if req.args.has_key('cancel'):
+            if 'cancel' in req.args:
                 if milestone.exists:
                     req.redirect(req.href.milestone(milestone.name))
                 else:
@@ -695,12 +703,33 @@ class MilestoneModule(Component):
     def _do_delete(self, req, milestone):
         req.perm(milestone.resource).require('MILESTONE_DELETE')
 
-        retarget_to = None
-        if req.args.has_key('retarget'):
-            retarget_to = req.args.get('target') or None
-        milestone.delete(retarget_to, req.authname)
+        retarget_to = req.args.get('target') or None
+        # Don't translate ticket comment (comment:40:ticket:5658)
+        retargeted_tickets = \
+            milestone.move_tickets(retarget_to, req.authname,
+                "Ticket retargeted after milestone deleted")
+        milestone.delete(author=req.authname)
         add_notice(req, _('The milestone "%(name)s" has been deleted.',
                           name=milestone.name))
+        if retargeted_tickets:
+            add_notice(req, _('The tickets associated with milestone '
+                              '"%(name)s" have been retargeted to milestone '
+                              '"%(retarget)s".', name=milestone.name,
+                              retarget=retarget_to))
+            new_values = {'milestone': retarget_to}
+            comment = _("Tickets retargeted after milestone deleted")
+            tn = BatchTicketNotifyEmail(self.env)
+            try:
+                tn.notify(retargeted_tickets, new_values, comment, None,
+                          req.authname)
+            except Exception, e:
+                self.log.error("Failure sending notification on ticket batch "
+                               "change: %s", exception_to_unicode(e))
+                add_warning(req, tag_("The changes have been saved, but an "
+                                      "error occurred while sending "
+                                      "notifications: %(message)s",
+                                      message=to_unicode(e)))
+
         req.redirect(req.href.roadmap())
 
     def _do_save(self, req, milestone):
@@ -722,7 +751,7 @@ class MilestoneModule(Component):
             milestone.due = None
 
         completed = req.args.get('completeddate', '')
-        retarget_to = req.args.get('target')
+        retarget_to = req.args.get('target') or None
 
         # 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
@@ -764,15 +793,31 @@ class MilestoneModule(Component):
 
         # -- actually save changes
         if milestone.exists:
-            milestone.update()
-            # eventually retarget opened tickets associated with the milestone
-            if 'retarget' in req.args and completed:
-                self.env.db_transaction("""
-                    UPDATE ticket SET milestone=%s
-                    WHERE milestone=%s and status != 'closed'
-                    """, (retarget_to, old_name))
-                self.log.info("Tickets associated with milestone %s "
-                              "retargeted to %s" % (old_name, retarget_to))
+            milestone.update(author=req.authname)
+            if completed and 'retarget' in req.args:
+                comment = req.args.get('comment', '')
+                retargeted_tickets = \
+                    milestone.move_tickets(retarget_to, req.authname,
+                                           comment, exclude_closed=True)
+                add_notice(req, _('The open tickets associated with '
+                                  'milestone "%(name)s" have been retargeted '
+                                  'to milestone "%(retarget)s".',
+                                  name=milestone.name, retarget=retarget_to))
+                new_values = {'milestone': retarget_to}
+                comment = comment or \
+                          _("Open tickets retargeted after milestone closed")
+                tn = BatchTicketNotifyEmail(self.env)
+                try:
+                    tn.notify(retargeted_tickets, new_values, comment, None,
+                              req.authname)
+                except Exception, e:
+                    self.log.error("Failure sending notification on ticket "
+                                   "batch change: %s",
+                                   exception_to_unicode(e))
+                    add_warning(req, tag_("The changes have been saved, but "
+                                          "an error occurred while sending "
+                                          "notifications: %(message)s",
+                                          message=to_unicode(e)))
         else:
             milestone.insert()
 
@@ -816,6 +861,9 @@ class MilestoneModule(Component):
                 'TICKET_ADMIN' in req.perm)
         else:
             req.perm(milestone.resource).require('MILESTONE_CREATE')
+            if milestone.name:
+                add_notice(req, _("Milestone %(name)s does not exist. You can"
+                                  " create it here.", name=milestone.name))
 
         chrome = Chrome(self.env)
         chrome.add_jquery_ui(req)
@@ -910,7 +958,7 @@ class MilestoneModule(Component):
     def _render_link(self, context, name, label, extra=''):
         try:
             milestone = Milestone(self.env, name)
-        except TracError:
+        except ResourceNotFound:
             milestone = None
         # Note: the above should really not be needed, `Milestone.exists`
         # should simply be false if the milestone doesn't exist in the db
@@ -918,9 +966,29 @@ class MilestoneModule(Component):
         href = context.href.milestone(name)
         if milestone and milestone.exists:
             if 'MILESTONE_VIEW' in context.perm(milestone.resource):
+                title = None
+                if hasattr(context, 'req'):
+                    if milestone.is_completed:
+                        title = _(
+                            'Completed %(duration)s ago (%(date)s)',
+                            duration=pretty_timedelta(milestone.completed),
+                            date=user_time(context.req, format_datetime,
+                                           milestone.completed))
+                    elif milestone.is_late:
+                        title = _('%(duration)s late (%(date)s)',
+                                  duration=pretty_timedelta(milestone.due),
+                                  date=user_time(context.req, format_datetime,
+                                                 milestone.due))
+                    elif milestone.due:
+                        title = _('Due in %(duration)s (%(date)s)',
+                                  duration=pretty_timedelta(milestone.due),
+                                  date=user_time(context.req, format_datetime,
+                                                 milestone.due))
+                    else:
+                        title = _('No date set')
                 closed = 'closed ' if milestone.is_completed else ''
                 return tag.a(label, class_='%smilestone' % closed,
-                             href=href + extra)
+                             href=href + extra, title=title)
         elif 'MILESTONE_CREATE' in context.perm('milestone', name):
             return tag.a(label, class_='missing milestone', href=href + extra,
                          rel='nofollow')
@@ -972,7 +1040,7 @@ class MilestoneModule(Component):
         milestone_realm = Resource('milestone')
         for name, due, completed, description \
                 in MilestoneCache(self.env).milestones.itervalues():
-            if any(r.search(description) or r.search(name)
+            if all(r.search(description) or r.search(name)
                    for r in term_regexps):
                 milestone = milestone_realm(id=name)
                 if 'MILESTONE_VIEW' in req.perm(milestone):

Modified: 
bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/templates/batch_modify.html
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/templates/batch_modify.html?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- 
bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/templates/batch_modify.html
 (original)
+++ 
bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/templates/batch_modify.html
 Sat Nov 15 01:14:46 2014
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2012-2014 Edgewall Software
+
+  This software is licensed as described in the file COPYING, which
+  you should have received as part of this distribution. The terms
+  are also available at http://trac.edgewall.com/license.html.
+
+  This software consists of voluntary contributions made by many
+  individuals. For the exact contribution history, see the revision
+  history and logs, available at http://trac.edgewall.org/.
+-->
 <form xmlns="http://www.w3.org/1999/xhtml";
      xmlns:py="http://genshi.edgewall.org/";
      xmlns:i18n="http://genshi.edgewall.org/i18n";
@@ -12,7 +22,7 @@
         <label for="batchmod_value_comment">Comment:</label>
       </th>
       <td class="fullrow"><textarea
-          id="batchmod_value_comment" name="batchmod_value_comment" cols="70" 
rows="5"/>
+          id="batchmod_value_comment" name="batchmod_value_comment" 
class="trac-fullwidth" cols="70" rows="5"/>
       </td>
     </tr>
 
@@ -54,7 +64,7 @@
   <div>
     <input type="hidden" name="selected_tickets" value=""/>
     <input type="hidden" name="query_href" value="${query_href}"/>
-    <input type="submit" id="batchmod_submit" name="batchmod_submit" 
value="${_('Change tickets')}" />
+    <input type="submit" id="batchmod_submit" name="batchmod_submit" 
class="trac-disable-on-submit" value="${_('Change tickets')}" />
   </div>
 
 </fieldset>

Modified: 
bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/templates/milestone_delete.html
URL: 
http://svn.apache.org/viewvc/bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/templates/milestone_delete.html?rev=1639823&r1=1639822&r2=1639823&view=diff
==============================================================================
--- 
bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/templates/milestone_delete.html
 (original)
+++ 
bloodhound/branches/trac-1.0.2-integration/trac/trac/ticket/templates/milestone_delete.html
 Sat Nov 15 01:14:46 2014
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  This software is licensed as described in the file COPYING, which
+  you should have received as part of this distribution. The terms
+  are also available at http://trac.edgewall.com/license.html.
+
+  This software consists of voluntary contributions made by many
+  individuals. For the exact contribution history, see the revision
+  history and logs, available at http://trac.edgewall.org/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd";>
@@ -10,11 +20,6 @@
     <title i18n:msg="name">Delete Milestone ${milestone.name}</title>
     <link rel="stylesheet" type="text/css"
           href="${chrome.htdocs_location}css/roadmap.css" />
-    <script type="text/javascript">
-      jQuery(document).ready(function($) {
-        $("#retarget").click(function(){ $("#target").enable(this.checked) });
-      });
-    </script>
   </head>
 
   <body>
@@ -25,7 +30,6 @@
       <div>
         <input type="hidden" name="action" value="delete" />
         <p><strong>Are you sure you want to delete this milestone?</strong></p>
-        <input type="checkbox" id="retarget" name="retarget" checked="checked" 
/>
         <label for="target">Retarget associated tickets to milestone</label>
         <select name="target" id="target">
           <option value="">None</option>
@@ -37,8 +41,8 @@
         </select>
       </div>
       <div class="buttons">
+        <input type="submit" id="delete" class="trac-disable-on-submit" 
value="${_('Delete milestone')}" />
         <input type="submit" name="cancel" value="${_('Cancel')}" />
-        <input type="submit" value="${_('Delete milestone')}" />
       </div>
     </form>
 


Reply via email to