bloodhound-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From g..@apache.org
Subject svn commit: r1340948 [6/6] - in /incubator/bloodhound/vendor/trac/current: ./ contrib/ trac/ trac/admin/tests/ trac/htdocs/ trac/htdocs/css/ trac/htdocs/js/ trac/locale/ trac/locale/en_GB/LC_MESSAGES/ trac/locale/fr/LC_MESSAGES/ trac/locale/hu/LC_MESSA...
Date Mon, 21 May 2012 09:23:03 GMT
Modified: incubator/bloodhound/vendor/trac/current/trac/tests/attachment.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/tests/attachment.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/tests/attachment.py (original)
+++ incubator/bloodhound/vendor/trac/current/trac/tests/attachment.py Mon May 21 09:23:01 2012
@@ -13,6 +13,22 @@ from trac.resource import Resource, reso
 from trac.test import EnvironmentStub
 
 
+hashes = {
+    '42': '92cfceb39d57d914ed8b14d0e37643de0797ae56',
+    'Foo.Mp3': '95797b6eb253337ff2c54e0881e2b747ec394f51',
+    'SomePage': 'd7e80bae461ca8568e794792f5520b603f540e06',
+    'Teh bar.jpg': 'ed9102c4aa099e92baf1073f824d21c5e4be5944',
+    'Teh foo.txt': 'ab97ba98d98fcf72b92e33a66b07077010171f70',
+    'bar.7z': '6c9600ad4d59ac864e6f0d2030c1fc76b4b406cb',
+    'bar.jpg': 'ae0faa593abf2b6f8871f6f32fe5b28d1c6572be',
+    'foo.$$$': 'eefc6aa745dbe129e8067a4a57637883edd83a8a',
+    'foo.2.txt': 'a8fcfcc2ef4e400ee09ae53c1aabd7f5a5fda0c7',
+    'foo.txt': '9206ac42b532ef8e983470c251f4e1a365fd636c',
+    u'bar.aäc': '70d0e3b813fdc756602d82748719a3ceb85cbf29',
+    u'ÜberSicht': 'a16c6837f6d3d2cc3addd68976db1c55deb694c8',
+}
+
+
 class TicketOnlyViewsTicket(Component):
     implements(IPermissionPolicy)
 
@@ -29,7 +45,8 @@ class AttachmentTestCase(unittest.TestCa
         self.env = EnvironmentStub()
         self.env.path = os.path.join(tempfile.gettempdir(), 'trac-tempenv')
         os.mkdir(self.env.path)
-        self.attachments_dir = os.path.join(self.env.path, 'attachments')
+        self.attachments_dir = os.path.join(self.env.path, 'files',
+                                            'attachments')
         self.env.config.set('trac', 'permission_policies',
                             'TicketOnlyViewsTicket, LegacyAttachmentPolicy')
         self.env.config.set('attachment', 'max_size', 512)
@@ -43,25 +60,59 @@ class AttachmentTestCase(unittest.TestCa
     def test_get_path(self):
         attachment = Attachment(self.env, 'ticket', 42)
         attachment.filename = 'foo.txt'
-        self.assertEqual(os.path.join(self.attachments_dir, 'ticket', '42',
-                                      'foo.txt'),
+        self.assertEqual(os.path.join(self.attachments_dir, 'ticket',
+                                      hashes['42'][0:3], hashes['42'],
+                                      hashes['foo.txt'] + '.txt'),
                          attachment.path)
         attachment = Attachment(self.env, 'wiki', 'SomePage')
         attachment.filename = 'bar.jpg'
-        self.assertEqual(os.path.join(self.attachments_dir, 'wiki', 'SomePage',
-                                      'bar.jpg'),
+        self.assertEqual(os.path.join(self.attachments_dir, 'wiki',
+                                      hashes['SomePage'][0:3],
+                                      hashes['SomePage'],
+                                      hashes['bar.jpg'] + '.jpg'),
+                         attachment.path)
+
+    def test_path_extension(self):
+        attachment = Attachment(self.env, 'ticket', 42)
+        attachment.filename = 'Foo.Mp3'
+        self.assertEqual(os.path.join(self.attachments_dir, 'ticket',
+                                      hashes['42'][0:3], hashes['42'],
+                                      hashes['Foo.Mp3'] + '.Mp3'),
+                         attachment.path)
+        attachment = Attachment(self.env, 'wiki', 'SomePage')
+        attachment.filename = 'bar.7z'
+        self.assertEqual(os.path.join(self.attachments_dir, 'wiki',
+                                      hashes['SomePage'][0:3],
+                                      hashes['SomePage'],
+                                      hashes['bar.7z'] + '.7z'),
+                         attachment.path)
+        attachment = Attachment(self.env, 'ticket', 42)
+        attachment.filename = 'foo.$$$'
+        self.assertEqual(os.path.join(self.attachments_dir, 'ticket',
+                                      hashes['42'][0:3], hashes['42'],
+                                      hashes['foo.$$$']),
+                         attachment.path)
+        attachment = Attachment(self.env, 'wiki', 'SomePage')
+        attachment.filename = u'bar.aäc'
+        self.assertEqual(os.path.join(self.attachments_dir, 'wiki',
+                                      hashes['SomePage'][0:3],
+                                      hashes['SomePage'],
+                                      hashes[u'bar.aäc']),
                          attachment.path)
 
     def test_get_path_encoded(self):
         attachment = Attachment(self.env, 'ticket', 42)
         attachment.filename = 'Teh foo.txt'
-        self.assertEqual(os.path.join(self.attachments_dir, 'ticket', '42',
-                                      'Teh%20foo.txt'),
+        self.assertEqual(os.path.join(self.attachments_dir, 'ticket',
+                                      hashes['42'][0:3], hashes['42'],
+                                      hashes['Teh foo.txt'] + '.txt'),
                          attachment.path)
         attachment = Attachment(self.env, 'wiki', u'ÜberSicht')
         attachment.filename = 'Teh bar.jpg'
         self.assertEqual(os.path.join(self.attachments_dir, 'wiki',
-                                      '%C3%9CberSicht', 'Teh%20bar.jpg'),
+                                      hashes[u'ÜberSicht'][0:3],
+                                      hashes[u'ÜberSicht'],
+                                      hashes['Teh bar.jpg'] + '.jpg'),
                          attachment.path)
 
     def test_select_empty(self):
@@ -88,6 +139,11 @@ class AttachmentTestCase(unittest.TestCa
         attachment = Attachment(self.env, 'ticket', 42)
         attachment.insert('foo.txt', StringIO(''), 0)
         self.assertEqual('foo.2.txt', attachment.filename)
+        self.assertEqual(os.path.join(self.attachments_dir, 'ticket',
+                                      hashes['42'][0:3], hashes['42'],
+                                      hashes['foo.2.txt'] + '.txt'),
+                         attachment.path)
+        self.assert_(os.path.exists(attachment.path))
 
     def test_insert_outside_attachments_dir(self):
         attachment = Attachment(self.env, '../../../../../sth/private', 42)

Modified: incubator/bloodhound/vendor/trac/current/trac/ticket/api.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/ticket/api.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/ticket/api.py (original)
+++ incubator/bloodhound/vendor/trac/current/trac/ticket/api.py Mon May 21 09:23:01 2012
@@ -419,12 +419,13 @@ class TicketSystem(Component):
     def get_permission_actions(self):
         return ['TICKET_APPEND', 'TICKET_CREATE', 'TICKET_CHGPROP',
                 'TICKET_VIEW', 'TICKET_EDIT_CC', 'TICKET_EDIT_DESCRIPTION',
-                'TICKET_EDIT_COMMENT',
+                'TICKET_EDIT_COMMENT', 'TICKET_BATCH_MODIFY',
                 ('TICKET_MODIFY', ['TICKET_APPEND', 'TICKET_CHGPROP']),
                 ('TICKET_ADMIN', ['TICKET_CREATE', 'TICKET_MODIFY',
                                   'TICKET_VIEW', 'TICKET_EDIT_CC',
                                   'TICKET_EDIT_DESCRIPTION',
-                                  'TICKET_EDIT_COMMENT'])]
+                                  'TICKET_EDIT_COMMENT',
+                                  'TICKET_BATCH_MODIFY'])]
 
     # IWikiSyntaxProvider methods
 

Added: incubator/bloodhound/vendor/trac/current/trac/ticket/batch.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/ticket/batch.py?rev=1340948&view=auto
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/ticket/batch.py (added)
+++ incubator/bloodhound/vendor/trac/current/trac/ticket/batch.py Mon May 21 09:23:01 2012
@@ -0,0 +1,206 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010 Brian Meeker
+# All rights reserved.
+#
+# 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.org/wiki/TracLicense.
+#
+# 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/log/.
+#
+# Author: Brian Meeker <meeker.brian@gmail.com>
+
+from __future__ import with_statement
+
+import re
+from datetime import datetime
+
+from genshi.builder import tag
+
+from trac.core import *
+from trac.ticket import TicketSystem, Ticket
+from trac.ticket.notification import BatchTicketNotifyEmail
+from trac.util.datefmt import utc
+from trac.util.text import exception_to_unicode, to_unicode
+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.
+    
+    This component allows multiple tickets to be modified in one request from
+    the custom query page. For users with the TICKET_BATCH_MODIFY permission
+    it will add a [TracBatchModify batch modify] section underneath custom
+    query results. Users can choose which tickets and fields they wish to
+    modify.
+    """
+    
+    implements(IRequestHandler)
+    
+    fields_as_list = ['keywords', 'cc']
+    list_separator_re =  re.compile(r'[;\s,]+')
+    list_connector_string = ', '
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        return req.path_info == '/batchmodify'
+
+    def process_request(self, req):
+        req.perm.assert_permission('TICKET_BATCH_MODIFY')
+
+        comment = req.args.get('batchmod_value_comment', '')
+        action = req.args.get('action')
+        
+        new_values = self._get_new_ticket_values(req) 
+        selected_tickets = self._get_selected_tickets(req)
+
+        self._save_ticket_changes(req, selected_tickets,
+                                  new_values, comment, action) 
+                
+        #Always redirect back to the query page we came from.
+        req.redirect(req.session['query_href'])
+
+    def _get_new_ticket_values(self, req):
+        """Pull all of the new values out of the post data."""
+        values = {}
+        
+        for field in TicketSystem(self.env).get_ticket_fields():
+            name = field['name']
+            if name not in ('id', 'resolution', 'status', 'owner', 'time',
+                            'changetime', 'summary', 'reporter',
+                            'description') and field['type'] != 'text-area':
+                value = req.args.get('batchmod_value_' + name)
+                if value is not None:
+                    values[name] = value
+        return values
+
+    def _get_selected_tickets(self, req):
+        """The selected tickets will be a comma separated list
+        in the request arguments."""
+        selected_tickets = req.args.get('selected_tickets')
+        if selected_tickets == '':
+            return []
+        else:
+            return selected_tickets.split(',')
+
+    def add_template_data(self, req, data, tickets):
+        data['batch_modify'] = True
+        data['query_href'] = req.session['query_href'] or req.href.query()
+        data['action_controls'] = self._get_action_controls(req, tickets)
+        batch_list_modes = [
+            {'name': _("add"), 'value': "+"},
+            {'name': _("remove"), 'value': "-"},
+            {'name': _("add / remove"), 'value': "+-"},
+            {'name': _("set to"), 'value': "="},
+        ]
+        add_script_data(req, batch_modify=True,
+                             batch_list_modes=batch_list_modes,
+                             batch_list_properties=self.fields_as_list)
+
+    def _get_action_controls(self, req, tickets):
+        action_controls = []
+        ts = TicketSystem(self.env)        
+        tickets_by_action = {}
+        for t in tickets:
+            ticket = Ticket(self.env, t['id'])
+            actions = ts.get_available_actions(req, ticket)
+            for action in actions:
+                tickets_by_action.setdefault(action, []).append(ticket)
+        sorted_actions = sorted(set(tickets_by_action.keys()))
+        for action in sorted_actions:
+            first_label = None
+            hints = []
+            widgets = []
+            ticket = tickets_by_action[action][0]
+            for controller in self._get_action_controllers(req, ticket,
+                                                           action):
+                label, widget, hint = controller.render_ticket_action_control(
+                    req, ticket, action)
+                if not first_label:
+                    first_label = label
+                widgets.append(widget)
+                hints.append(hint)
+            action_controls.append((action, first_label, tag(widgets), hints))
+        return action_controls
+
+    def _get_action_controllers(self, req, ticket, action):
+        """Generator yielding the controllers handling the given `action`"""
+        for controller in TicketSystem(self.env).action_controllers:
+            actions = [a for w, a in
+                       controller.get_ticket_actions(req, ticket) or []]
+            if action in actions:
+                yield controller
+
+    def _save_ticket_changes(self, req, selected_tickets, 
+                             new_values, comment, action):
+        """Save all of the changes to tickets."""
+        when = datetime.now(utc)
+        with self.env.db_transaction as db:
+            for id in selected_tickets:
+                t = Ticket(self.env, int(id))
+                _values = new_values.copy()
+                for field in self.fields_as_list:
+                    if field in new_values:
+                        old = t.values[field] if field in t.values else ''
+                        new = new_values[field]
+                        mode = req.args.get('batchmod_value_' + field +
+                                            '_mode')
+                        new2 = req.args.get('batchmod_value_' + field +
+                                            '_secondary', '')
+                        _values[field] = self._change_list(old, new, new2,
+                                                           mode)
+                controllers = list(self._get_action_controllers(req, t,
+                                                                action))
+                for controller in controllers:
+                    _values.update(controller.get_ticket_changes(req, t, 
+                                                                 action))
+                t.populate(_values)
+                t.save_changes(req.authname, comment, when=when)
+                for controller in controllers:
+                    controller.apply_action_side_effects(req, t, action)
+        try:
+            tn = BatchTicketNotifyEmail(self.env)
+            tn.notify(selected_tickets, new_values, comment, action,
+                      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)))
+    
+    def _change_list(self, old_list, new_list, new_list2, mode):
+        changed_list = [k.strip()
+                        for k in self.list_separator_re.split(old_list)
+                        if k]
+        new_list = [k.strip()
+                    for k in self.list_separator_re.split(new_list)
+                    if k]
+        new_list2 = [k.strip()
+                     for k in self.list_separator_re.split(new_list2)
+                     if k]
+        
+        if mode == '=':
+            changed_list = new_list
+        elif mode ==  '+':
+            for entry in new_list:
+                if entry not in changed_list:
+                    changed_list.append(entry)
+        elif mode == '-':
+            for entry in new_list:
+                while entry in changed_list:
+                    changed_list.remove(entry)
+        elif mode == '+-':
+            for entry in new_list:
+                if entry not in changed_list:
+                    changed_list.append(entry)
+            for entry in new_list2:
+                while entry in changed_list:
+                    changed_list.remove(entry)
+        return self.list_connector_string.join(changed_list)

Propchange: incubator/bloodhound/vendor/trac/current/trac/ticket/batch.py
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: incubator/bloodhound/vendor/trac/current/trac/ticket/default_workflow.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/ticket/default_workflow.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/ticket/default_workflow.py (original)
+++ incubator/bloodhound/vendor/trac/current/trac/ticket/default_workflow.py Mon May 21 09:23:01 2012
@@ -485,7 +485,7 @@ class WorkflowMacro(WikiMacroBase):
         graph = {'nodes': states, 'actions': action_names, 'edges': edges,
                  'width': args.get('width', 800), 
                  'height': args.get('height', 600)}
-        graph_id = '%.8x' % id(graph)
+        graph_id = '%012x' % id(graph)
         req = formatter.req
         add_script(req, 'common/js/excanvas.js', ie_if='IE')
         add_script(req, 'common/js/workflow_graph.js')

Modified: incubator/bloodhound/vendor/trac/current/trac/ticket/notification.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/ticket/notification.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/ticket/notification.py (original)
+++ incubator/bloodhound/vendor/trac/current/trac/ticket/notification.py Mon May 21 09:23:01 2012
@@ -55,6 +55,13 @@ class TicketNotificationSystem(Component
         By default, the subject template is `$prefix #$ticket.id: $summary`.
         `$prefix` being the value of the `smtp_subject_prefix` option.
         ''(since 0.11)''""")
+        
+    batch_subject_template = Option('notification', 'batch_subject_template', 
+                                     '$prefix Batch modify: $tickets_descr',
+        """Like ticket_subject_template but for batch modifications.
+
+        By default, the template is `$prefix Batch modify: $tickets_descr`.
+        ''(since 0.13)''""")
 
     ambiguous_char_width = Option('notification', 'ambiguous_char_width',
                                   'single',
@@ -66,6 +73,61 @@ 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)
+        
 
 class TicketNotifyEmail(NotifyEmail):
     """Notification of ticket changes."""
@@ -321,61 +383,11 @@ class TicketNotifyEmail(NotifyEmail):
         return template.generate(**data).render('text', encoding=None).strip()
 
     def get_recipients(self, tktid):
-        notify_reporter = self.config.getbool('notification',
-                                              'always_notify_reporter')
-        notify_owner = self.config.getbool('notification',
-                                           'always_notify_owner')
-        notify_updater = self.config.getbool('notification', 
-                                             'always_notify_updater')
-
-        ccrecipients = self.prev_cc
-        torecipients = []
-        with self.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() 
-                self.reporter = row[1]
-                self.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 == self.reporter):
-                    filter_out = False
-                if notify_owner and (updater == self.owner):
-                    filter_out = False
-                if filter_out:
-                    torecipients = [r for r in torecipients 
-                                    if r and r != updater]
-            elif updater:
-                torecipients.append(updater)
-
+        (torecipients, ccrecipients, reporter, owner) = \
+            get_ticket_notification_recipients(self.env, self.config, 
+                tktid, self.prev_cc)
+        self.reporter = reporter
+        self.owner = owner
         return (torecipients, ccrecipients)
 
     def get_message_id(self, rcpt, modtime=None):
@@ -412,3 +424,67 @@ class TicketNotifyEmail(NotifyEmail):
             return text
         else:
             return obfuscate_email_address(text)
+
+class BatchTicketNotifyEmail(NotifyEmail):
+    """Notification of ticket batch modifications."""
+
+    template_name = "batch_ticket_notify_email.txt"
+
+    def __init__(self, env):
+        NotifyEmail.__init__(self, env)
+
+    def notify(self, tickets, new_values, comment, action, author):
+        """Send batch ticket change notification e-mail (untranslated)"""
+        t = deactivate()
+        try:
+            self._notify(tickets, new_values, comment, action, author)
+        finally:
+            reactivate(t)
+
+    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)
+                                  for (prop, val) in new_values.iteritems()])
+        tickets_descr = ', '.join(['#%s' % t for t in tickets])
+        subject = self.format_subj(tickets_descr)
+        link = self.env.abs_href.query(id=','.join([str(t) for t in tickets]))
+        self.data.update({
+            'tickets_descr': tickets_descr,
+            'changes_descr': changes_descr,
+            'comment': comment,
+            'action': action,
+            'author': author,
+            'subject': subject,
+            'ticket_query_link': link,
+            })
+        NotifyEmail.notify(self, tickets, subject, author)
+
+    def format_subj(self, tickets_descr):
+        template = self.config.get('notification','batch_subject_template')
+        template = NewTextTemplate(template.encode('utf8'))
+                                                
+        prefix = self.config.get('notification', 'smtp_subject_prefix')
+        if prefix == '__default__': 
+            prefix = '[%s]' % self.env.project_name
+        
+        data = {
+            'prefix': prefix,
+            'tickets_descr': tickets_descr,
+            'env': self.env,
+        }
+        
+        return template.generate(**data).render('text', encoding=None).strip()
+
+    def get_recipients(self, tktids):
+        alltorecipients = []
+        allccrecipients = []
+        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)))

Modified: incubator/bloodhound/vendor/trac/current/trac/ticket/query.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/ticket/query.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/ticket/query.py (original)
+++ incubator/bloodhound/vendor/trac/current/trac/ticket/query.py Mon May 21 09:23:01 2012
@@ -32,7 +32,7 @@ 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
+from trac.ticket.model import Milestone, group_milestones, Ticket
 from trac.util import Ranges, as_bool
 from trac.util.datefmt import format_datetime, from_utimestamp, parse_date, \
                               to_timestamp, to_utimestamp, utc, user_time
@@ -1100,6 +1100,13 @@ class QueryModule(Component):
                     data['description'] = description
         else:
             data['report_href'] = None
+
+        # Only interact with the batch modify module it it is enabled
+        from trac.ticket.batch import BatchModifyModule
+        if 'TICKET_BATCH_MODIFY' in req.perm and \
+                self.env.is_component_enabled(BatchModifyModule):
+            self.env[BatchModifyModule].add_template_data(req, data, tickets)
+            
         data.setdefault('report', None)
         data.setdefault('description', None)
         data['title'] = title

Added: incubator/bloodhound/vendor/trac/current/trac/ticket/templates/batch_modify.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/ticket/templates/batch_modify.html?rev=1340948&view=auto
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/ticket/templates/batch_modify.html (added)
+++ incubator/bloodhound/vendor/trac/current/trac/ticket/templates/batch_modify.html Mon May 21 09:23:01 2012
@@ -0,0 +1,60 @@
+<form xmlns="http://www.w3.org/1999/xhtml"
+     xmlns:py="http://genshi.edgewall.org/"
+     xmlns:i18n="http://genshi.edgewall.org/i18n"
+     xmlns:xi="http://www.w3.org/2001/XInclude"
+     id="batchmod_form" method="post" action="${req.href + '/batchmodify'}">
+    
+<fieldset id="batchmod_fieldset">
+  <legend class="foldable">Batch Modify</legend>
+  <table summary="Batch modification fields">
+    <tr id="batchmod_comment">
+      <th colspan="2">
+        <label for="batchmod_value_comment">Comment:</label>
+      </th>
+      <td class="fullrow"><textarea
+          id="batchmod_value_comment" name="batchmod_value_comment" cols="70" rows="5"/>
+      </td>
+    </tr>
+    
+    <tr id="add_batchmod_field_row">
+      <td colspan="3">
+        <label class="batchmod_label" for="add_batchmod_field">Add Field:</label>
+        <select id="add_batchmod_field"
+                py:with="field_names = sorted(fields.iterkeys(), key=lambda name: fields[name].label.lower())">
+          <option></option>
+          <option py:for="field_name in field_names" py:with="field = fields[field_name]"
+                  py:if="field_name not in ('id', 'resolution', 'status', 'owner', 'time', 'changetime', 'summary', 'reporter', 'description') and fields[field_name].type != 'text-area'"
+                  value="$field_name">${field.label}</option>
+        </select>
+      </td>
+    </tr>
+    
+    <tr>
+      <td colspan="3">
+        <fieldset id="batchmod_action"> 
+          <legend>Action</legend>
+          <div py:for="key, label, controls, hints in action_controls">
+            <input type="radio" id="action_$key" name="action" value="$key"
+                   checked="${key == action or None}" />
+            <label for="action_$key">$label</label>
+            $controls
+            <span class="hint" py:for="hint in hints">$hint</span>
+          </div>
+        </fieldset>
+      </td>
+    </tr>
+  </table>
+  
+  <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')}" />
+  </div>
+  
+  <div id="batchmod_help" i18n:msg="">
+    <strong>Note:</strong> See <a href="${href.wiki('TracBatchModify')}">TracBatchModify</a> for help on using batch modify.
+  </div>
+
+</fieldset>
+
+</form>

Propchange: incubator/bloodhound/vendor/trac/current/trac/ticket/templates/batch_modify.html
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: incubator/bloodhound/vendor/trac/current/trac/ticket/templates/batch_modify.html
------------------------------------------------------------------------------
    svn:mime-type = text/html

Added: incubator/bloodhound/vendor/trac/current/trac/ticket/templates/batch_ticket_notify_email.txt
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/ticket/templates/batch_ticket_notify_email.txt?rev=1340948&view=auto
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/ticket/templates/batch_ticket_notify_email.txt (added)
+++ incubator/bloodhound/vendor/trac/current/trac/ticket/templates/batch_ticket_notify_email.txt Mon May 21 09:23:01 2012
@@ -0,0 +1,17 @@
+${_('Batch modification to %(tickets)s by %(author)s:',
+      tickets=tickets_descr, author=author)}
+$changes_descr
+{%if action %}\
+
+${_('Action: %(action)s', action=action)}
+{% end %}\
+{%if comment %}\
+
+${_('Comment:')}
+$comment
+{% end %}\
+
+-- 
+${_('Tickets URL: <%(link)s>', link=ticket_query_link)}
+$project.name <${project.url or abs_href()}>
+$project.descr

Propchange: incubator/bloodhound/vendor/trac/current/trac/ticket/templates/batch_ticket_notify_email.txt
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: incubator/bloodhound/vendor/trac/current/trac/ticket/templates/query.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/ticket/templates/query.html?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/ticket/templates/query.html (original)
+++ incubator/bloodhound/vendor/trac/current/trac/ticket/templates/query.html Mon May 21 09:23:01 2012
@@ -12,6 +12,9 @@
     <script type="text/javascript">/*<![CDATA[*/
       jQuery(document).ready(function($) {
         initializeFilters();
+        if(batch_modify) {
+          initializeBatch();
+        }
         $("#group").change(function() {
           $("#groupdesc").enable(this.selectedIndex != 0)
         }).change();
@@ -218,6 +221,7 @@
       </form>
 
       <xi:include href="query_results.html" />
+      <xi:include py:if="batch_modify" href="batch_modify.html" />
 
       <div class="buttons"
            py:with="edit = report_resource and 'REPORT_MODIFY' in perm(report_resource);

Modified: incubator/bloodhound/vendor/trac/current/trac/ticket/tests/__init__.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/ticket/tests/__init__.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/ticket/tests/__init__.py (original)
+++ incubator/bloodhound/vendor/trac/current/trac/ticket/tests/__init__.py Mon May 21 09:23:01 2012
@@ -3,7 +3,7 @@ import unittest
 
 import trac.ticket
 from trac.ticket.tests import api, model, query, wikisyntax, notification, \
-                              conversion, report, roadmap
+                              conversion, report, roadmap, batch
 from trac.ticket.tests.functional import functionalSuite
 
 def suite():
@@ -16,6 +16,7 @@ def suite():
     suite.addTest(conversion.suite())
     suite.addTest(report.suite())
     suite.addTest(roadmap.suite())
+    suite.addTest(batch.suite())
     suite.addTest(doctest.DocTestSuite(trac.ticket.api))
     suite.addTest(doctest.DocTestSuite(trac.ticket.report))
     suite.addTest(doctest.DocTestSuite(trac.ticket.roadmap))

Added: incubator/bloodhound/vendor/trac/current/trac/ticket/tests/batch.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/ticket/tests/batch.py?rev=1340948&view=auto
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/ticket/tests/batch.py (added)
+++ incubator/bloodhound/vendor/trac/current/trac/ticket/tests/batch.py Mon May 21 09:23:01 2012
@@ -0,0 +1,256 @@
+from trac.perm import PermissionCache
+from trac.test import Mock, EnvironmentStub
+from trac.ticket import default_workflow, web_ui
+from trac.ticket.batch import BatchModifyModule
+from trac.ticket.model import Ticket
+from trac.util.datefmt import utc
+
+import unittest
+
+
+class BatchModifyTestCase(unittest.TestCase):
+    
+    def setUp(self):
+        self.env = EnvironmentStub(default_data=True,
+            enable=[default_workflow.ConfigurableTicketWorkflow,
+                    web_ui.TicketModule])
+        self.req = Mock(href=self.env.href, authname='anonymous', tz=utc)
+        self.req.session = {}
+        self.req.perm = PermissionCache(self.env)
+    
+    def assertCommentAdded(self, ticket_id, comment):
+        ticket = Ticket(self.env, int(ticket_id))
+        changes = ticket.get_changelog()
+        comment_change = [c for c in changes if c[2] == 'comment'][0]
+        self.assertEqual(comment_change[2], comment)
+    
+    def assertFieldChanged(self, ticket_id, field, new_value):
+        ticket = Ticket(self.env, int(ticket_id))
+        changes = ticket.get_changelog()
+        field_change = [c for c in changes if c[2] == field][0]
+        self.assertEqual(field_change[4], new_value)
+    
+    def _change_list_test_helper(self, original, new, new2, mode):
+        batch = BatchModifyModule(self.env)
+        return batch._change_list(original, new, new2, mode)
+        
+    def _add_list_test_helper(self, original, to_add):
+        return self._change_list_test_helper(original, to_add, '', '+')
+        
+    def _remove_list_test_helper(self, original, to_remove):
+        return self._change_list_test_helper(original, to_remove, '', '-')
+        
+    def _add_remove_list_test_helper(self, original, to_add, to_remove):
+        return self._change_list_test_helper(original, to_add, to_remove,
+                                             '+-')
+        
+    def _assign_list_test_helper(self, original, new):
+        return self._change_list_test_helper(original, new, '', '=')        
+    
+    def _insert_ticket(self, summary, **kw):
+        """Helper for inserting a ticket into the database"""
+        ticket = Ticket(self.env)
+        for k, v in kw.items():
+            ticket[k] = v
+        return ticket.insert()
+    
+    def test_ignore_summary_reporter_and_description(self):
+        """These cannot be added through the UI, but if somebody tries
+        to build their own POST data they will be ignored."""
+        batch = BatchModifyModule(self.env)
+        self.req.args = {}
+        self.req.args['batchmod_value_summary'] = 'test ticket'
+        self.req.args['batchmod_value_reporter'] = 'anonymous'
+        self.req.args['batchmod_value_description'] = 'synergize the widgets'
+        values = batch._get_new_ticket_values(self.req)
+        self.assertEqual(len(values), 0)
+        
+    def test_add_batchmod_value_data_from_request(self):
+        batch = BatchModifyModule(self.env)
+        self.req.args = {}
+        self.req.args['batchmod_value_milestone'] = 'milestone1'
+        values = batch._get_new_ticket_values(self.req)
+        self.assertEqual(values['milestone'], 'milestone1')
+        
+    def test_selected_tickets(self):
+        self.req.args = { 'selected_tickets' : '1,2,3' }        
+        batch = BatchModifyModule(self.env)
+        selected_tickets = batch._get_selected_tickets(self.req)
+        self.assertEqual(selected_tickets, ['1', '2', '3'])
+        
+    def test_no_selected_tickets(self):
+        """If nothing is selected, the return value is the empty list."""
+        self.req.args = { 'selected_tickets' : '' }        
+        batch = BatchModifyModule(self.env)
+        selected_tickets = batch._get_selected_tickets(self.req)
+        self.assertEqual(selected_tickets, [])
+
+    # Assign list items
+    
+    def test_change_list_replace_empty_with_single(self):
+        """Replace emtpy field with single item."""
+        changed = self._assign_list_test_helper('', 'alice')
+        self.assertEqual(changed, 'alice')
+        
+    def test_change_list_replace_empty_with_items(self):
+        """Replace emtpy field with items."""
+        changed = self._assign_list_test_helper('', 'alice, bob')
+        self.assertEqual(changed, 'alice, bob')
+        
+    def test_change_list_replace_item(self):
+        """Replace item with a different item."""
+        changed = self._assign_list_test_helper('alice', 'bob')
+        self.assertEqual(changed, 'bob')
+        
+    def test_change_list_replace_item_with_items(self):
+        """Replace item with different items."""
+        changed = self._assign_list_test_helper('alice', 'bob, carol')
+        self.assertEqual(changed, 'bob, carol')
+        
+    def test_change_list_replace_items_with_item(self):
+        """Replace items with a different item."""
+        changed = self._assign_list_test_helper('alice, bob', 'carol')
+        self.assertEqual(changed, 'carol')
+        
+    def test_change_list_replace_items(self):
+        """Replace items with different items."""
+        changed = self._assign_list_test_helper('alice, bob', 'carol, dave')
+        self.assertEqual(changed, 'carol, dave')
+        
+    def test_change_list_replace_items_partial(self):
+        """Replace items with different (or not) items."""
+        changed = self._assign_list_test_helper('alice, bob', 'bob, dave')
+        self.assertEqual(changed, 'bob, dave')
+        
+    def test_change_list_clear(self):
+        """Clear field."""
+        changed = self._assign_list_test_helper('alice bob', '')
+        self.assertEqual(changed, '')
+
+    # Add / remove list items
+    
+    def test_change_list_add_item(self):
+        """Append additional item."""
+        changed = self._add_list_test_helper('alice', 'bob')
+        self.assertEqual(changed, 'alice, bob')
+        
+    def test_change_list_add_items(self):
+        """Append additional items."""
+        changed = self._add_list_test_helper('alice, bob', 'carol, dave')
+        self.assertEqual(changed, 'alice, bob, carol, dave')
+        
+    def test_change_list_remove_item(self):
+        """Remove existing item."""
+        changed = self._remove_list_test_helper('alice, bob', 'bob')
+        self.assertEqual(changed, 'alice')
+        
+    def test_change_list_remove_items(self):
+        """Remove existing items."""
+        changed = self._remove_list_test_helper('alice, bob, carol',
+                                                'alice, carol')
+        self.assertEqual(changed, 'bob')
+        
+    def test_change_list_remove_idempotent(self):
+        """Ignore missing item to be removed."""
+        changed = self._remove_list_test_helper('alice', 'bob')
+        self.assertEqual(changed, 'alice')
+        
+    def test_change_list_remove_mixed(self):
+        """Ignore only missing item to be removed."""
+        changed = self._remove_list_test_helper('alice, bob', 'bob, carol')
+        self.assertEqual(changed, 'alice')
+        
+    def test_change_list_add_remove(self):
+        """Remove existing item and append additional item."""
+        changed = self._add_remove_list_test_helper('alice, bob', 'carol',
+                                                'alice')
+        self.assertEqual(changed, 'bob, carol')
+        
+    def test_change_list_add_no_duplicates(self):
+        """Existing items are not duplicated."""
+        changed = self._add_list_test_helper('alice, bob', 'bob, carol')
+        self.assertEqual(changed, 'alice, bob, carol')
+        
+    def test_change_list_remove_all_duplicates(self):
+        """Remove all duplicates."""
+        changed = self._remove_list_test_helper('alice, bob, alice', 'alice')
+        self.assertEqual(changed, 'bob')
+    
+    # Save
+    
+    def test_save_comment(self):
+        """Comments are saved to all selected tickets."""
+        first_ticket_id = self._insert_ticket('Test 1', reporter='joe')
+        second_ticket_id = self._insert_ticket('Test 2', reporter='joe')
+        selected_tickets = [first_ticket_id, second_ticket_id]
+        
+        batch = BatchModifyModule(self.env)
+        batch._save_ticket_changes(self.req, selected_tickets, {}, 'comment',
+                                   'leave')
+        
+        self.assertCommentAdded(first_ticket_id, 'comment')
+        self.assertCommentAdded(second_ticket_id, 'comment')
+    
+    def test_save_values(self):
+        """Changed values are saved to all tickets."""
+        first_ticket_id = self._insert_ticket('Test 1', reporter='joe', 
+                                              component='foo')
+        second_ticket_id = self._insert_ticket('Test 2', reporter='joe')
+        selected_tickets = [first_ticket_id, second_ticket_id]
+        new_values = { 'component' : 'bar' } 
+        
+        batch = BatchModifyModule(self.env)
+        batch._save_ticket_changes(self.req, selected_tickets, new_values, '',
+                                   'leave')
+        
+        self.assertFieldChanged(first_ticket_id, 'component', 'bar')
+        self.assertFieldChanged(second_ticket_id, 'component', 'bar')
+    
+    def test_action_with_state_change(self):
+        """Actions can have change status."""
+        self.env.config.set('ticket-workflow', 'embiggen', '* -> big')
+        
+        first_ticket_id = self._insert_ticket('Test 1', reporter='joe', 
+                                              status='small')
+        second_ticket_id = self._insert_ticket('Test 2', reporter='joe')
+        selected_tickets = [first_ticket_id, second_ticket_id]
+        
+        batch = BatchModifyModule(self.env)
+        batch._save_ticket_changes(self.req, selected_tickets, {}, '',
+                                   'embiggen')
+        
+        ticket = Ticket(self.env, int(first_ticket_id))
+        changes = ticket.get_changelog()
+        self.assertFieldChanged(first_ticket_id, 'status', 'big')
+        self.assertFieldChanged(second_ticket_id, 'status', 'big')
+    
+    def test_action_with_side_effects(self):
+        """Actions can have operations with side effects."""
+        self.env.config.set('ticket-workflow', 'buckify', '* -> *')
+        self.env.config.set('ticket-workflow', 'buckify.operations',
+                                               'set_owner')
+        self.req.args = {}
+        self.req.args['action_buckify_reassign_owner'] = 'buck'
+        
+        first_ticket_id = self._insert_ticket('Test 1', reporter='joe', 
+                                              owner='foo')
+        second_ticket_id = self._insert_ticket('Test 2', reporter='joe')
+        selected_tickets = [first_ticket_id, second_ticket_id]
+        
+        batch = BatchModifyModule(self.env)
+        batch._save_ticket_changes(self.req, selected_tickets, {}, '',
+                                   'buckify')
+        
+        ticket = Ticket(self.env, int(first_ticket_id))
+        changes = ticket.get_changelog()
+        self.assertFieldChanged(first_ticket_id, 'owner', 'buck')
+        self.assertFieldChanged(second_ticket_id, 'owner', 'buck')
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(BatchModifyTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')

Propchange: incubator/bloodhound/vendor/trac/current/trac/ticket/tests/batch.py
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: incubator/bloodhound/vendor/trac/current/trac/ticket/tests/functional.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/ticket/tests/functional.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/ticket/tests/functional.py (original)
+++ incubator/bloodhound/vendor/trac/current/trac/ticket/tests/functional.py Mon May 21 09:23:01 2012
@@ -241,6 +241,148 @@ class TestTicketQueryOrClause(Functional
             tc.find('TestTicketQueryOrClause%s' % i)
 
 
+class TestTicketCustomFieldTextNoFormat(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test custom text field with no format explicitly specified.
+        Its contents should be rendered as plain text.
+        """
+        env = self._testenv.get_trac_environment()
+        env.config.set('ticket-custom', 'newfield', 'text')
+        env.config.set('ticket-custom', 'newfield.label',
+                       'Another Custom Field')
+        env.config.set('ticket-custom', 'newfield.format', '')
+        env.config.save()
+
+        self._testenv.restart()
+        val = "%s %s" % (random_unique_camel(), random_word())
+        ticketid = self._tester.create_ticket(summary=random_sentence(3),
+                                              info={'newfield': val})
+        self._tester.go_to_ticket(ticketid)
+        tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % val)
+
+
+class TestTicketCustomFieldTextAreaNoFormat(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test custom textarea field with no format explicitly specified, 
+        its contents should be rendered as plain text.
+        """
+        env = self._testenv.get_trac_environment()
+        env.config.set('ticket-custom', 'newfield', 'textarea')
+        env.config.set('ticket-custom', 'newfield.label',
+                       'Another Custom Field')
+        env.config.set('ticket-custom', 'newfield.format', '')
+        env.config.save()
+
+        self._testenv.restart()
+        val = "%s %s" % (random_unique_camel(), random_word())
+        ticketid = self._tester.create_ticket(summary=random_sentence(3),
+                                              info={'newfield': val})
+        self._tester.go_to_ticket(ticketid)
+        tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % val)
+
+
+class TestTicketCustomFieldTextWikiFormat(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test custom text field with `wiki` format. 
+        Its contents should through the wiki engine, wiki-links and all.
+        Feature added in http://trac.edgewall.org/ticket/1791
+        """
+        env = self._testenv.get_trac_environment()
+        env.config.set('ticket-custom', 'newfield', 'text')
+        env.config.set('ticket-custom', 'newfield.label',
+                       'Another Custom Field')
+        env.config.set('ticket-custom', 'newfield.format', 'wiki')
+        env.config.save()
+
+        self._testenv.restart()
+        word1 = random_unique_camel()
+        word2 = random_word()
+        val = "%s %s" % (word1, word2)
+        ticketid = self._tester.create_ticket(summary=random_sentence(3),
+                                              info={'newfield': val})
+        self._tester.go_to_ticket(ticketid)
+        wiki = '<a [^>]*>%s\??</a> %s' % (word1, word2)
+        tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % wiki)
+
+
+class TestTicketCustomFieldTextAreaWikiFormat(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test custom textarea field with no format explicitly specified, 
+        its contents should be rendered as plain text.
+        """
+        env = self._testenv.get_trac_environment()
+        env.config.set('ticket-custom', 'newfield', 'textarea')
+        env.config.set('ticket-custom', 'newfield.label',
+                       'Another Custom Field')
+        env.config.set('ticket-custom', 'newfield.format', 'wiki')
+        env.config.save()
+
+        self._testenv.restart()
+        word1 = random_unique_camel()
+        word2 = random_word()
+        val = "%s %s" % (word1, word2)
+        ticketid = self._tester.create_ticket(summary=random_sentence(3),
+                                              info={'newfield': val})
+        self._tester.go_to_ticket(ticketid)
+        wiki = '<p>\s*<a [^>]*>%s\??</a> %s<br />\s*</p>' % (word1, word2)
+        tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % wiki)
+
+
+class TestTicketCustomFieldTextReferenceFormat(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test custom text field with `reference` format.
+        Its contents are treated as a single value
+        and are rendered as an auto-query link.
+        Feature added in http://trac.edgewall.org/ticket/10643
+        """
+        env = self._testenv.get_trac_environment()
+        env.config.set('ticket-custom', 'newfield', 'text')
+        env.config.set('ticket-custom', 'newfield.label',
+                       'Another Custom Field')
+        env.config.set('ticket-custom', 'newfield.format', 'reference')
+        env.config.save()
+
+        self._testenv.restart()
+        word1 = random_unique_camel()
+        word2 = random_word()
+        val = "%s %s" % (word1, word2)
+        ticketid = self._tester.create_ticket(summary=random_sentence(3),
+                                              info={'newfield': val})
+        self._tester.go_to_ticket(ticketid)
+        query = 'status=!closed&amp;newfield=%s\+%s' % (word1, word2)
+        querylink = '<a href="/query\?%s">%s</a>' % (query, val)
+        tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % querylink)
+
+
+class TestTicketCustomFieldTextListFormat(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test custom text field with `list` format. 
+        Its contents are treated as a space-separated list of values
+        and are rendered as separate auto-query links per word.
+        Feature added in http://trac.edgewall.org/ticket/10643
+        """
+        env = self._testenv.get_trac_environment()
+        env.config.set('ticket-custom', 'newfield', 'text')
+        env.config.set('ticket-custom', 'newfield.label',
+                       'Another Custom Field')
+        env.config.set('ticket-custom', 'newfield.format', 'list')
+        env.config.save()
+
+        self._testenv.restart()
+        word1 = random_unique_camel()
+        word2 = random_word()
+        val = "%s %s" % (word1, word2)
+        ticketid = self._tester.create_ticket(summary=random_sentence(3),
+                                              info={'newfield': val})
+        self._tester.go_to_ticket(ticketid)
+        query1 = 'status=!closed&amp;newfield=~%s' % word1
+        query2 = 'status=!closed&amp;newfield=~%s' % word2
+        querylink1 = '<a href="/query\?%s">%s</a>' % (query1, word1)
+        querylink2 = '<a href="/query\?%s">%s</a>' % (query2, word2)
+        querylinks = '%s %s' % (querylink1, querylink2)
+        tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % querylinks)
+
+
 class TestTimelineTicketDetails(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test ticket details on timeline"""
@@ -1501,6 +1643,12 @@ def functionalSuite(suite=None):
     suite.addTest(TestTicketHistoryDiff())
     suite.addTest(TestTicketQueryLinks())
     suite.addTest(TestTicketQueryOrClause())
+    suite.addTest(TestTicketCustomFieldTextNoFormat())
+    suite.addTest(TestTicketCustomFieldTextWikiFormat())
+    suite.addTest(TestTicketCustomFieldTextAreaNoFormat())
+    suite.addTest(TestTicketCustomFieldTextAreaWikiFormat())
+    suite.addTest(TestTicketCustomFieldTextReferenceFormat())
+    suite.addTest(TestTicketCustomFieldTextListFormat())
     suite.addTest(TestTimelineTicketDetails())
     suite.addTest(TestAdminComponent())
     suite.addTest(TestAdminComponentDuplicates())

Modified: incubator/bloodhound/vendor/trac/current/trac/ticket/web_ui.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/ticket/web_ui.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/ticket/web_ui.py (original)
+++ incubator/bloodhound/vendor/trac/current/trac/ticket/web_ui.py Mon May 21 09:23:01 2012
@@ -280,46 +280,76 @@ class TicketModule(Component):
                     (ticket, verb, info, summary, status, resolution, type,
                      description, comment, cid))
 
+        def produce_ticket_change_events(db):
+            data = None
+            for id, t, author, type, summary, field, oldvalue, newvalue \
+                    in db("""
+                    SELECT t.id, tc.time, tc.author, t.type, t.summary, 
+                           tc.field, tc.oldvalue, tc.newvalue 
+                    FROM ticket_change tc 
+                        INNER JOIN ticket t ON t.id = tc.ticket 
+                            AND tc.time>=%s AND tc.time<=%s 
+                    ORDER BY tc.time
+                    """ % (ts_start, ts_stop)):
+                if not (oldvalue or newvalue):
+                    # ignore empty change corresponding to custom field 
+                    # created (None -> '') or deleted ('' -> None)
+                    continue
+                if not data or (id, t) != data[:2]:
+                    if data:
+                        ev = produce_event(data, status, fields, comment,
+                                           cid)
+                        if ev:
+                             yield (ev, data[1])
+                    status, fields, comment, cid = 'edit', {}, '', None
+                    data = (id, t, author, type, summary, None)
+                if field == 'comment':
+                    comment = newvalue
+                    cid = oldvalue and oldvalue.split('.')[-1]
+                    # Always use the author from the comment field
+                    data = data[:2] + (author,) + data[3:]
+                elif field == 'status' and \
+                        newvalue in ('reopened', 'closed'):
+                    status = newvalue
+                elif field[0] != '_':
+                    # properties like _comment{n} are hidden
+                    fields[field] = newvalue
+            if data:
+                ev = produce_event(data, status, fields, comment, cid)
+                if ev:
+                    yield (ev, data[1])
+                     
         # Ticket changes
         with self.env.db_query as db:
             if 'ticket' in filters or 'ticket_details' in filters:
-                data = None
-                for id, t, author, type, summary, field, oldvalue, newvalue \
-                        in db("""
-                        SELECT t.id, tc.time, tc.author, t.type, t.summary, 
-                               tc.field, tc.oldvalue, tc.newvalue 
-                        FROM ticket_change tc 
-                            INNER JOIN ticket t ON t.id = tc.ticket 
-                                AND tc.time>=%s AND tc.time<=%s 
-                        ORDER BY tc.time
-                        """ % (ts_start, ts_stop)):
-                    if not (oldvalue or newvalue):
-                        # ignore empty change corresponding to custom field 
-                        # created (None -> '') or deleted ('' -> None)
-                        continue 
-                    if not data or (id, t) != data[:2]:
-                        if data:
-                            ev = produce_event(data, status, fields, comment,
-                                               cid)
-                            if ev:
-                                yield ev
-                        status, fields, comment, cid = 'edit', {}, '', None
-                        data = (id, t, author, type, summary, None)
-                    if field == 'comment':
-                        comment = newvalue
-                        cid = oldvalue and oldvalue.split('.')[-1]
-                        # Always use the author from the comment field
-                        data = data[:2] + (author,) + data[3:]
-                    elif field == 'status' and \
-                            newvalue in ('reopened', 'closed'):
-                        status = newvalue
-                    elif field[0] != '_':
-                        # properties like _comment{n} are hidden
-                        fields[field] = newvalue
-                if data:
-                    ev = produce_event(data, status, fields, comment, cid)
-                    if ev:
-                        yield ev
+                prev_t = None
+                prev_ev = None
+                batch_ev = None
+                for (ev, t) in produce_ticket_change_events(db):
+                    if batch_ev:
+                        if prev_t == t:
+                            ticket = ev[3][0]
+                            batch_ev[3][0].append(ticket.id)
+                        else:
+                            yield batch_ev
+                            prev_ev = ev
+                            prev_t = t
+                            batch_ev = None
+                    elif prev_t and prev_t == t:
+                        prev_ticket = prev_ev[3][0]
+                        ticket = ev[3][0]
+                        tickets = [prev_ticket.id, ticket.id]
+                        batch_data = (tickets,) + ev[3][1:]
+                        batch_ev = ('batchmodify', ev[1], ev[2], batch_data) 
+                    else:
+                        if prev_ev:
+                            yield prev_ev
+                        prev_ev = ev
+                        prev_t = t
+                if batch_ev:
+                    yield batch_ev
+                elif prev_ev:
+                    yield prev_ev
 
                 # New tickets
                 if 'ticket' in filters:
@@ -338,6 +368,9 @@ class TicketModule(Component):
                     yield event
 
     def render_timeline_event(self, context, field, event):
+        kind = event[0]
+        if kind == 'batchmodify':
+            return self._render_batched_timeline_event(context, field, event)
         ticket, verb, info, summary, status, resolution, type, \
                 description, comment, cid = event[3]
         if field == 'url':
@@ -367,6 +400,22 @@ class TicketModule(Component):
                                     shorten_lines=flavor == 'oneliner')
             return descr + format_to(self.env, None, t_context, message)
 
+    def _render_batched_timeline_event(self, context, field, event):
+        tickets, verb, info, summary, status, resolution, type, \
+                description, comment, cid = event[3]
+        tickets = sorted(tickets)
+        if field == 'url':
+            return context.href.query(id=','.join([str(t) for t in tickets]))
+        elif field == 'title':
+            ticketids = ','.join([str(t) for t in tickets])
+            title = _("Tickets %(ticketids)s", ticketids=ticketids)
+            return tag_("Tickets %(ticketlist)s batch updated",
+                        ticketlist=tag.em('#', ticketids, title=title))
+        elif field == 'description':
+            t_context = context()
+            t_context.set_hints(preserve_newlines=self.must_preserve_newlines)
+            return info + format_to(self.env, None, t_context, comment)
+
     # Internal methods
 
     def _get_action_controllers(self, req, ticket, action):
@@ -1440,6 +1489,12 @@ class TicketModule(Component):
                 if field.get('format') == 'wiki':
                     field['rendered'] = format_to_oneliner(self.env, context,
                                                            ticket[name])
+                elif field.get('format') == 'reference':
+                    field['rendered'] = self._query_link(req, name,
+                                                         ticket[name])
+                elif field.get('format') == 'list':
+                    field['rendered'] = self._query_link_words(context, name,
+                                                               ticket[name])
             elif type_ == 'textarea':
                 if field.get('format') == 'wiki':
                     field['rendered'] = \

Modified: incubator/bloodhound/vendor/trac/current/trac/versioncontrol/web_ui/changeset.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/versioncontrol/web_ui/changeset.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/versioncontrol/web_ui/changeset.py (original)
+++ incubator/bloodhound/vendor/trac/current/trac/versioncontrol/web_ui/changeset.py Mon May 21 09:23:01 2012
@@ -775,6 +775,7 @@ class ChangesetModule(Component):
                 # UTF-8 is not supported by all Zip tools either,
                 # but as some do, UTF-8 is the best option here.
                 zipinfo.filename = new_node.path.strip('/').encode('utf-8')
+                zipinfo.flag_bits |= 0x800 # filename is encoded with utf-8
                 zipinfo.date_time = new_node.last_modified.utctimetuple()[:6]
                 zipinfo.compress_type = compression
                 # setting zipinfo.external_attr is needed since Python 2.5

Modified: incubator/bloodhound/vendor/trac/current/trac/web/api.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/web/api.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/web/api.py (original)
+++ incubator/bloodhound/vendor/trac/current/trac/web/api.py Mon May 21 09:23:01 2012
@@ -459,6 +459,11 @@ class Request(object):
             scheme, host = urlparse.urlparse(self.base_url)[:2]
             url = urlparse.urlunparse((scheme, host, url, None, None, None))
 
+        # Workaround #10382, IE6+ bug when post and redirect with hash
+        if status == 303 and '#' in url and \
+                ' MSIE ' in self.environ.get('HTTP_USER_AGENT', ''):
+            url = url.replace('#', '#__msie303:')
+
         self.send_header('Location', url)
         self.send_header('Content-Type', 'text/plain')
         self.send_header('Content-Length', 0)

Modified: incubator/bloodhound/vendor/trac/current/trac/web/auth.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/web/auth.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/web/auth.py (original)
+++ incubator/bloodhound/vendor/trac/current/trac/web/auth.py Mon May 21 09:23:01 2012
@@ -181,6 +181,8 @@ class LoginModule(Component):
                                              or req.base_path or '/'
         if self.env.secure_cookies:
             req.outcookie['trac_auth']['secure'] = True
+        if sys.version_info >= (2, 6):
+            req.outcookie['trac_auth']['httponly'] = True
         if self.auth_cookie_lifetime > 0:
             req.outcookie['trac_auth']['expires'] = self.auth_cookie_lifetime
 
@@ -217,6 +219,8 @@ class LoginModule(Component):
         req.outcookie['trac_auth']['expires'] = -10000
         if self.env.secure_cookies:
             req.outcookie['trac_auth']['secure'] = True
+        if sys.version_info >= (2, 6):
+            req.outcookie['trac_auth']['httponly'] = True
 
     def _cookie_to_name(self, req, cookie):
         # This is separated from _get_name_for_cookie(), because the

Modified: incubator/bloodhound/vendor/trac/current/trac/web/chrome.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/web/chrome.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/web/chrome.py (original)
+++ incubator/bloodhound/vendor/trac/current/trac/web/chrome.py Mon May 21 09:23:01 2012
@@ -373,14 +373,14 @@ class Chrome(Component):
         rules will be needed in the web server.""")
 
     jquery_location = Option('trac', 'jquery_location', '',
-        """Location of the jQuery !JavaScript library (version 1.5.1).
+        """Location of the jQuery !JavaScript library (version 1.7.2).
         
         An empty value loads jQuery from the copy bundled with Trac.
         
         Alternatively, jQuery could be loaded from a CDN, for example:
-        http://code.jquery.com/jquery-1.5.1.min.js,
-        http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.5.1.min.js or
-        https://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js.
+        http://code.jquery.com/jquery-1.7.2.min.js,
+        http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.2.min.js or
+        https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js.
         
         (''since 0.13'')""")
 

Modified: incubator/bloodhound/vendor/trac/current/trac/web/main.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/web/main.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/web/main.py (original)
+++ incubator/bloodhound/vendor/trac/current/trac/web/main.py Mon May 21 09:23:01 2012
@@ -299,6 +299,8 @@ class RequestDispatcher(Component):
             req.outcookie['trac_form_token']['path'] = req.base_path or '/'
             if self.env.secure_cookies:
                 req.outcookie['trac_form_token']['secure'] = True
+            if sys.version_info >= (2, 6):
+                req.outcookie['trac_form_token']['httponly'] = True
             return req.outcookie['trac_form_token'].value
 
     def _pre_process_request(self, req, chosen_handler):

Modified: incubator/bloodhound/vendor/trac/current/trac/web/session.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/web/session.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/web/session.py (original)
+++ incubator/bloodhound/vendor/trac/current/trac/web/session.py Mon May 21 09:23:01 2012
@@ -20,6 +20,7 @@
 
 from __future__ import with_statement
 
+import sys
 import time
 
 from trac.admin.api import console_date_format
@@ -204,6 +205,8 @@ class Session(DetachedSession):
         self.req.outcookie[COOKIE_KEY]['expires'] = expires
         if self.env.secure_cookies:
             self.req.outcookie[COOKIE_KEY]['secure'] = True
+        if sys.version_info >= (2, 6):
+            self.req.outcookie[COOKIE_KEY]['httponly'] = True
 
     def get_session(self, sid, authenticated=False):
         refresh_cookie = False

Added: incubator/bloodhound/vendor/trac/current/trac/wiki/default-pages/TracBatchModify
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/wiki/default-pages/TracBatchModify?rev=1340948&view=auto
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/wiki/default-pages/TracBatchModify (added)
+++ incubator/bloodhound/vendor/trac/current/trac/wiki/default-pages/TracBatchModify Mon May 21 09:23:01 2012
@@ -0,0 +1,10 @@
+= Trac Ticket Batch Modification =
+[[TracGuideToc]]
+
+From [wiki:TracQuery custom query] results Trac provides support for modifying a batch of tickets in one request.
+
+To perform a batch modification select the tickets you wish to modify and set the new field values using the section underneath the query results. 
+
+== List fields
+
+The `Keywords` and `Cc` fields are treated as lists, where list items can be added and / or removed in addition of replacing the entire list value. All list field controls accept multiple items (i.e. multiple keywords or cc addresses).
\ No newline at end of file

Modified: incubator/bloodhound/vendor/trac/current/trac/wiki/default-pages/TracGuide
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/wiki/default-pages/TracGuide?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/wiki/default-pages/TracGuide (original)
+++ incubator/bloodhound/vendor/trac/current/trac/wiki/default-pages/TracGuide Mon May 21 09:23:01 2012
@@ -19,6 +19,7 @@ Currently available documentation:
      * TracReports — Writing and using reports.
      * TracQuery — Executing custom ticket queries.
      * TracRoadmap — The roadmap helps tracking project progress.
+     * TracBatchModify - Modifying a batch of tickets in one request.
  * '''Administrator Guide'''
    * TracInstall — How to install and run Trac.
    * TracUpgrade — How to upgrade existing installations.

Modified: incubator/bloodhound/vendor/trac/current/trac/wiki/default-pages/TracTicketsCustomFields
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/wiki/default-pages/TracTicketsCustomFields?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/wiki/default-pages/TracTicketsCustomFields (original)
+++ incubator/bloodhound/vendor/trac/current/trac/wiki/default-pages/TracTicketsCustomFields Mon May 21 09:23:01 2012
@@ -17,7 +17,11 @@ The example below should help to explain
    * label: Descriptive label.
    * value: Default value.
    * order: Sort order placement. (Determines relative placement in forms with respect to other custom fields.)
-   * format: Either `plain` for plain text or `wiki` to interpret the content as WikiFormatting. (''since 0.11.3'')
+   * format: One of:
+     * `plain` for plain text
+     * `wiki` to interpret the content as WikiFormatting (''since 0.11.3'')
+     * `reference` to treat the content as a queryable value (''since 0.13'')
+     * `list` to interpret the content as a list of queryable values, separated by whitespace (''since 0.13'')
  * '''checkbox''': A boolean value check box.
    * label: Descriptive label.
    * value: Default value (0 or 1).

Modified: incubator/bloodhound/vendor/trac/current/trac/wiki/macros.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/trac/wiki/macros.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/trac/wiki/macros.py (original)
+++ incubator/bloodhound/vendor/trac/current/trac/wiki/macros.py Mon May 21 09:23:01 2012
@@ -827,6 +827,7 @@ class TracGuideTocMacro(WikiMacroBase):
            ('TracWorkflow',                 'Workflow'),
            ('TracRoadmap',                  'Roadmap'),
            ('TracQuery',                    'Ticket Queries'),
+           ('TracBatchModify',              'Batch Modify'),
            ('TracReports',                  'Reports'),
            ('TracRss',                      'RSS Support'),
            ('TracNotification',             'Notification'),

Modified: incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/git/PyGIT.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/git/PyGIT.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/git/PyGIT.py (original)
+++ incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/git/PyGIT.py Mon May 21 09:23:01 2012
@@ -71,7 +71,7 @@ class GitCore(object):
 
         #print >>sys.stderr, "DEBUG:", git_cmd, cmd_args
 
-        p = self.__pipe(git_cmd, *cmd_args, stdout=PIPE, stderr=PIPE)
+        p = self.__pipe(git_cmd, stdout=PIPE, stderr=PIPE, *cmd_args)
 
         stdout_data, stderr_data = p.communicate()
         #TODO, do something with p.returncode, e.g. raise exception
@@ -82,7 +82,7 @@ class GitCore(object):
         return self.__pipe('cat-file', '--batch', stdin=PIPE, stdout=PIPE)
 
     def log_pipe(self, *cmd_args):
-        return self.__pipe('log', *cmd_args, stdout=PIPE)
+        return self.__pipe('log', stdout=PIPE, *cmd_args)
 
     def __getattr__(self, name):
         if name[0] == '_' or name in ['cat_file_batch', 'log_pipe']:
@@ -829,15 +829,13 @@ class Storage(object):
             """
 
             def terminate_win(process):
-                import win32api, win32pdhutil, win32con, pywintypes
-                try:
-                    handle = win32api.OpenProcess(win32con.PROCESS_TERMINATE,
-                                                  0, process.pid)
-                    win32api.TerminateProcess(handle, -1)
-                    win32api.CloseHandle(handle)
-                except pywintypes.error:
-                    # Windows tends to throw access denied errors
-                    pass
+                import ctypes
+                PROCESS_TERMINATE = 1
+                handle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE,
+                                                            False,
+                                                            process.pid)
+                ctypes.windll.kernel32.TerminateProcess(handle, -1)
+                ctypes.windll.kernel32.CloseHandle(handle)
 
             def terminate_nix(process):
                 import os

Modified: incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/git/git_fs.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/git/git_fs.py?rev=1340948&r1=1340947&r2=1340948&view=diff
==============================================================================
--- incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/git/git_fs.py (original)
+++ incubator/bloodhound/vendor/trac/current/tracopt/versioncontrol/git/git_fs.py Mon May 21 09:23:01 2012
@@ -12,6 +12,8 @@
 # individuals. For the exact contribution history, see the revision
 # history and logs, available at http://trac.edgewall.org/log/.
 
+from __future__ import with_statement 
+
 from datetime import datetime
 import os
 import sys



Mime
View raw message