You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by gj...@apache.org on 2012/05/21 11:23:03 UTC
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...
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 <me...@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&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&newfield=~%s' % word1
+ query2 = 'status=!closed&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