bloodhound-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From g..@apache.org
Subject svn commit: r1340960 [6/6] - in /incubator/bloodhound/trunk/trac: ./ 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_MESSAGES/ trac...
Date Mon, 21 May 2012 10:22:31 GMT
Modified: incubator/bloodhound/trunk/trac/trac/tests/attachment.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/tests/attachment.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/tests/attachment.py (original)
+++ incubator/bloodhound/trunk/trac/trac/tests/attachment.py Mon May 21 10:22:29 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/trunk/trac/trac/ticket/api.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/ticket/api.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/ticket/api.py (original)
+++ incubator/bloodhound/trunk/trac/trac/ticket/api.py Mon May 21 10:22:29 2012
@@ -477,12 +477,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
 

Modified: incubator/bloodhound/trunk/trac/trac/ticket/default_workflow.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/ticket/default_workflow.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/ticket/default_workflow.py (original)
+++ incubator/bloodhound/trunk/trac/trac/ticket/default_workflow.py Mon May 21 10:22:29 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/trunk/trac/trac/ticket/notification.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/ticket/notification.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/ticket/notification.py (original)
+++ incubator/bloodhound/trunk/trac/trac/ticket/notification.py Mon May 21 10:22:29 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/trunk/trac/trac/ticket/query.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/ticket/query.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/ticket/query.py (original)
+++ incubator/bloodhound/trunk/trac/trac/ticket/query.py Mon May 21 10:22:29 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_date, format_datetime, from_utimestamp, \
                               parse_date, to_timestamp, to_utimestamp, utc, \
@@ -1105,6 +1105,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

Modified: incubator/bloodhound/trunk/trac/trac/ticket/templates/query.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/ticket/templates/query.html?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/ticket/templates/query.html (original)
+++ incubator/bloodhound/trunk/trac/trac/ticket/templates/query.html Mon May 21 10:22:29 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/trunk/trac/trac/ticket/tests/__init__.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/ticket/tests/__init__.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/ticket/tests/__init__.py (original)
+++ incubator/bloodhound/trunk/trac/trac/ticket/tests/__init__.py Mon May 21 10:22:29 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))

Modified: incubator/bloodhound/trunk/trac/trac/ticket/tests/functional.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/ticket/tests/functional.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/ticket/tests/functional.py (original)
+++ incubator/bloodhound/trunk/trac/trac/ticket/tests/functional.py Mon May 21 10:22:29 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/trunk/trac/trac/ticket/web_ui.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/ticket/web_ui.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/ticket/web_ui.py (original)
+++ incubator/bloodhound/trunk/trac/trac/ticket/web_ui.py Mon May 21 10:22:29 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/trunk/trac/trac/versioncontrol/web_ui/changeset.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/versioncontrol/web_ui/changeset.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/versioncontrol/web_ui/changeset.py (original)
+++ incubator/bloodhound/trunk/trac/trac/versioncontrol/web_ui/changeset.py Mon May 21 10:22:29 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/trunk/trac/trac/web/api.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/web/api.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/web/api.py (original)
+++ incubator/bloodhound/trunk/trac/trac/web/api.py Mon May 21 10:22:29 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/trunk/trac/trac/web/auth.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/web/auth.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/web/auth.py (original)
+++ incubator/bloodhound/trunk/trac/trac/web/auth.py Mon May 21 10:22:29 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/trunk/trac/trac/web/chrome.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/web/chrome.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/web/chrome.py (original)
+++ incubator/bloodhound/trunk/trac/trac/web/chrome.py Mon May 21 10:22:29 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/trunk/trac/trac/web/main.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/web/main.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/web/main.py (original)
+++ incubator/bloodhound/trunk/trac/trac/web/main.py Mon May 21 10:22:29 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/trunk/trac/trac/web/session.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/web/session.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/web/session.py (original)
+++ incubator/bloodhound/trunk/trac/trac/web/session.py Mon May 21 10:22:29 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

Modified: incubator/bloodhound/trunk/trac/trac/wiki/default-pages/TracGuide
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/wiki/default-pages/TracGuide?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/wiki/default-pages/TracGuide (original)
+++ incubator/bloodhound/trunk/trac/trac/wiki/default-pages/TracGuide Mon May 21 10:22:29 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/trunk/trac/trac/wiki/default-pages/TracTicketsCustomFields
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/wiki/default-pages/TracTicketsCustomFields?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/wiki/default-pages/TracTicketsCustomFields (original)
+++ incubator/bloodhound/trunk/trac/trac/wiki/default-pages/TracTicketsCustomFields Mon May 21 10:22:29 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/trunk/trac/trac/wiki/macros.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/wiki/macros.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/wiki/macros.py (original)
+++ incubator/bloodhound/trunk/trac/trac/wiki/macros.py Mon May 21 10:22:29 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/trunk/trac/tracopt/versioncontrol/git/PyGIT.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/PyGIT.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/PyGIT.py (original)
+++ incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/PyGIT.py Mon May 21 10:22:29 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/trunk/trac/tracopt/versioncontrol/git/git_fs.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/git_fs.py?rev=1340960&r1=1340959&r2=1340960&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/git_fs.py (original)
+++ incubator/bloodhound/trunk/trac/tracopt/versioncontrol/git/git_fs.py Mon May 21 10:22:29 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