1
2
3
4
5
6
7
8
9
10
11
12
13
14 import unittest
15 from datetime import timedelta
16
17 from trac.core import Component, implements
18 from trac.perm import DefaultPermissionPolicy, DefaultPermissionStore, \
19 PermissionSystem
20 from trac.test import EnvironmentStub, MockRequest
21 from trac.ticket import default_workflow, api, web_ui
22 from trac.ticket.batch import BatchModifyModule
23 from trac.ticket.model import Ticket
24 from trac.util.datefmt import datetime_now, utc
25 from trac.web.api import HTTPBadRequest, RequestDone
26 from trac.web.chrome import web_context
27 from trac.web.session import DetachedSession
28
29
31
41
47
53
57
59 return self._change_list_test_helper(original, to_add, '', '+')
60
62 return self._change_list_test_helper(original, to_remove, '', '-')
63
65 return self._change_list_test_helper(original, to_add, to_remove,
66 '+-')
67
69 return self._change_list_test_helper(original, new, '', '=')
70
77
79 """These cannot be added through the UI, but if somebody tries
80 to build their own POST data they will be ignored."""
81 batch = BatchModifyModule(self.env)
82 self.req.args = {
83 'batchmod_value_summary': 'test ticket',
84 'batchmod_value_reporter': 'anonymous',
85 'batchmod_value_description': 'synergize the widgets'
86 }
87 values = batch._get_new_ticket_values(self.req)
88 self.assertEqual(len(values), 0)
89
95
101
103 """If nothing is selected, the return value is the empty list."""
104 self.req.args = {'selected_tickets': ''}
105 batch = BatchModifyModule(self.env)
106 selected_tickets = batch._get_selected_tickets(self.req)
107 self.assertEqual(selected_tickets, [])
108
110 batch = BatchModifyModule(self.env)
111
112 req = MockRequest(self.env, method='GET', path_info='/batchmodify')
113 req.session['query_href'] = req.href.query()
114 self.assertTrue(batch.match_request(req))
115 self.assertRaises(HTTPBadRequest, batch.process_request, req)
116
117 req = MockRequest(self.env, method='POST', path_info='/batchmodify',
118 args={'selected_tickets': ''})
119 req.session['query_href'] = req.href.query()
120 self.assertTrue(batch.match_request(req))
121 self.assertRaises(RequestDone, batch.process_request, req)
122
124 redirect_listener_args = []
125 def redirect_listener(req, url, permanent):
126 redirect_listener_args[:] = (url, permanent)
127
128 batch = BatchModifyModule(self.env)
129 req = MockRequest(self.env, method='POST', path_info='/batchmodify')
130 query_opened_tickets = req.href.query(status='!closed')
131 query_default = req.href.query()
132 req.args = {'selected_tickets': '', 'query_href': query_opened_tickets}
133 req.session['query_href'] = query_default
134 req.add_redirect_listener(redirect_listener)
135
136 self.assertTrue(batch.match_request(req))
137 self.assertRaises(RequestDone, batch.process_request, req)
138 self.assertEqual([query_opened_tickets, False], redirect_listener_args)
139
140
141
143 """Replace empty field with single item."""
144 changed = self._assign_list_test_helper('', 'alice')
145 self.assertEqual(changed, 'alice')
146
148 """Replace empty field with items."""
149 changed = self._assign_list_test_helper('', 'alice, bob')
150 self.assertEqual(changed, 'alice, bob')
151
153 """Replace item with a different item."""
154 changed = self._assign_list_test_helper('alice', 'bob')
155 self.assertEqual(changed, 'bob')
156
158 """Replace item with different items."""
159 changed = self._assign_list_test_helper('alice', 'bob, carol')
160 self.assertEqual(changed, 'bob, carol')
161
163 """Replace items with a different item."""
164 changed = self._assign_list_test_helper('alice, bob', 'carol')
165 self.assertEqual(changed, 'carol')
166
168 """Replace items with different items."""
169 changed = self._assign_list_test_helper('alice, bob', 'carol, dave')
170 self.assertEqual(changed, 'carol, dave')
171
173 """Replace items with different (or not) items."""
174 changed = self._assign_list_test_helper('alice, bob', 'bob, dave')
175 self.assertEqual(changed, 'bob, dave')
176
178 """Clear field."""
179 changed = self._assign_list_test_helper('alice bob', '')
180 self.assertEqual(changed, '')
181
182
183
185 """Append additional item."""
186 changed = self._add_list_test_helper('alice', 'bob')
187 self.assertEqual(changed, 'alice, bob')
188
190 """Append additional items."""
191 changed = self._add_list_test_helper('alice, bob', 'carol, dave')
192 self.assertEqual(changed, 'alice, bob, carol, dave')
193
195 """Remove existing item."""
196 changed = self._remove_list_test_helper('alice, bob', 'bob')
197 self.assertEqual(changed, 'alice')
198
200 """Remove existing items."""
201 changed = self._remove_list_test_helper('alice, bob, carol',
202 'alice, carol')
203 self.assertEqual(changed, 'bob')
204
206 """Ignore missing item to be removed."""
207 changed = self._remove_list_test_helper('alice', 'bob')
208 self.assertEqual(changed, 'alice')
209
211 """Ignore only missing item to be removed."""
212 changed = self._remove_list_test_helper('alice, bob', 'bob, carol')
213 self.assertEqual(changed, 'alice')
214
216 """Remove existing item and append additional item."""
217 changed = self._add_remove_list_test_helper('alice, bob', 'carol',
218 'alice')
219 self.assertEqual(changed, 'bob, carol')
220
222 """Existing items are not duplicated."""
223 changed = self._add_list_test_helper('alice, bob', 'bob, carol')
224 self.assertEqual(changed, 'alice, bob, carol')
225
227 """Remove all duplicates."""
228 changed = self._remove_list_test_helper('alice, bob, alice', 'alice')
229 self.assertEqual(changed, 'bob')
230
231
232
245
247 """Changed values are saved to all tickets."""
248 first_ticket_id = self._insert_ticket('Test 1', reporter='joe',
249 component='foo')
250 second_ticket_id = self._insert_ticket('Test 2', reporter='joe')
251 selected_tickets = [first_ticket_id, second_ticket_id]
252 new_values = {'component': 'bar'}
253
254 batch = BatchModifyModule(self.env)
255 batch._save_ticket_changes(self.req, selected_tickets, new_values, '',
256 'leave')
257
258 self.assertFieldChanged(first_ticket_id, 'component', 'bar')
259 self.assertFieldChanged(second_ticket_id, 'component', 'bar')
260
262 batch = BatchModifyModule(self.env)
263 with self.env.db_transaction:
264 ticket_ids = [
265 self._insert_ticket('Test 1', reporter='joe', keywords='foo'),
266 self._insert_ticket('Test 2', reporter='joe', keywords='baz'),
267 ]
268
269 self.req.args = {'action': 'leave',
270 'batchmod_mode_keywords': '+',
271 'batchmod_primary_keywords': 'baz new',
272 'batchmod_secondary_keywords': '*****'}
273 batch._save_ticket_changes(self.req, ticket_ids, {}, '', 'leave')
274 self.assertFieldChanged(ticket_ids[0], 'keywords', 'foo, baz, new')
275 self.assertFieldChanged(ticket_ids[1], 'keywords', 'baz, new')
276
277 self.req.args = {'action': 'leave',
278 'batchmod_mode_keywords': '+-',
279 'batchmod_primary_keywords': 'one two three',
280 'batchmod_secondary_keywords': 'baz missing'}
281 batch._save_ticket_changes(self.req, ticket_ids, {}, '', 'leave')
282 self.assertFieldChanged(ticket_ids[0], 'keywords',
283 'foo, new, one, two, three')
284 self.assertFieldChanged(ticket_ids[1], 'keywords',
285 'new, one, two, three')
286
287 self.req.args = {'action': 'leave',
288 'batchmod_mode_keywords': '-',
289 'batchmod_primary_keywords': 'new two',
290 'batchmod_secondary_keywords': '*****'}
291 batch._save_ticket_changes(self.req, ticket_ids, {}, '', 'leave')
292 self.assertFieldChanged(ticket_ids[0], 'keywords', 'foo, one, three')
293 self.assertFieldChanged(ticket_ids[1], 'keywords', 'one, three')
294
295 self.req.args = {'action': 'leave',
296 'batchmod_mode_keywords': '=',
297 'batchmod_primary_keywords': 'orange',
298 'batchmod_secondary_keywords': '*****'}
299 batch._save_ticket_changes(self.req, ticket_ids, {}, '', 'leave')
300 self.assertFieldChanged(ticket_ids[0], 'keywords', 'orange')
301 self.assertFieldChanged(ticket_ids[1], 'keywords', 'orange')
302
304 """Actions can have change status."""
305 self.env.config.set('ticket-workflow', 'embiggen', '* -> big')
306
307 first_ticket_id = self._insert_ticket('Test 1', reporter='joe',
308 status='small')
309 second_ticket_id = self._insert_ticket('Test 2', reporter='joe')
310 selected_tickets = [first_ticket_id, second_ticket_id]
311
312 batch = BatchModifyModule(self.env)
313 batch._save_ticket_changes(self.req, selected_tickets, {}, '',
314 'embiggen')
315
316 self.assertFieldChanged(first_ticket_id, 'status', 'big')
317 self.assertFieldChanged(second_ticket_id, 'status', 'big')
318
320 """Actions can have operations with side effects."""
321 self.env.config.set('ticket-workflow', 'buckify', '* -> *')
322 self.env.config.set('ticket-workflow', 'buckify.operations',
323 'set_owner')
324 self.req.args = {'action_buckify_reassign_owner': 'buck'}
325
326 first_ticket_id = self._insert_ticket('Test 1', reporter='joe',
327 owner='foo')
328 second_ticket_id = self._insert_ticket('Test 2', reporter='joe')
329 selected_tickets = [first_ticket_id, second_ticket_id]
330
331 batch = BatchModifyModule(self.env)
332 batch._save_ticket_changes(self.req, selected_tickets, {}, '',
333 'buckify')
334
335 self.assertFieldChanged(first_ticket_id, 'owner', 'buck')
336 self.assertFieldChanged(second_ticket_id, 'owner', 'buck')
337
339 """Regression test for #11288"""
340 tktmod = web_ui.TicketModule(self.env)
341 now = datetime_now(utc)
342 start = now - timedelta(hours=1)
343 stop = now + timedelta(hours=1)
344 events = tktmod.get_timeline_events(self.req, start, stop,
345 ['ticket_details'])
346 self.assertEqual(True, all(ev[0] != 'batchmodify' for ev in events))
347
348 prio_ids = {}
349 for i in xrange(20):
350 t = Ticket(self.env)
351 t['summary'] = 'Ticket %d' % i
352 t['priority'] = ('', 'minor', 'major', 'critical')[i % 4]
353 tktid = t.insert()
354 prio_ids.setdefault(t['priority'], []).append(tktid)
355 tktids = prio_ids['critical'] + prio_ids['major'] + \
356 prio_ids['minor'] + prio_ids['']
357
358 new_values = {'summary': 'batch updated ticket',
359 'owner': 'ticket11288', 'reporter': 'ticket11288'}
360 batch = BatchModifyModule(self.env)
361 batch._save_ticket_changes(self.req, tktids, new_values, '', 'leave')
362
363 with self.env.db_transaction as db:
364 rows = db('SELECT * FROM ticket_change')
365 db.execute('DELETE FROM ticket_change')
366 rows = rows[0::4] + rows[1::4] + rows[2::4] + rows[3::4]
367 db.executemany('INSERT INTO ticket_change VALUES (%s)' %
368 ','.join(('%s',) * len(rows[0])),
369 rows)
370
371 events = tktmod.get_timeline_events(self.req, start, stop,
372 ['ticket_details'])
373 events = [ev for ev in events if ev[0] == 'batchmodify']
374 self.assertEqual(1, len(events))
375 batch_ev = events[0]
376 self.assertEqual('anonymous', batch_ev[2])
377 self.assertEqual(tktids, batch_ev[3][0])
378 self.assertEqual('updated', batch_ev[3][1])
379
380 context = web_context(self.req)
381 self.assertEqual(
382 self.req.href.query(id=','.join(str(t) for t in tktids)),
383 tktmod.render_timeline_event(context, 'url', batch_ev))
384
385
387
389 self.env = EnvironmentStub(default_data=True, enable=[
390 default_workflow.ConfigurableTicketWorkflow,
391 DefaultPermissionPolicy, DefaultPermissionStore,
392 BatchModifyModule, api.TicketSystem, web_ui.TicketModule
393 ])
394 self.env.config.set('trac', 'permission_policies',
395 'DefaultPermissionPolicy')
396 ps = PermissionSystem(self.env)
397 ps.grant_permission('has_ta_&_bm', 'TICKET_ADMIN')
398 ps.grant_permission('has_bm', 'TICKET_BATCH_MODIFY')
399 ps.grant_permission('has_ta_&_bm', 'TICKET_BATCH_MODIFY')
400 session = DetachedSession(self.env, 'has_ta_&_bm')
401 session.set('query_href', '')
402 session.save()
403 session = DetachedSession(self.env, 'has_bm')
404 session.set('query_href', '')
405 session.save()
406
409
413
421
423 """User with TICKET_ADMIN can batch modify the reporter."""
424 self._insert_ticket('Ticket 1', reporter='user1')
425 self._insert_ticket('Ticket 2', reporter='user1')
426
427 req = MockRequest(self.env, method='POST', authname='has_ta_&_bm',
428 args={
429 'batchmod_value_reporter': 'user2',
430 'batchmod_value_comment': '',
431 'action': 'leave',
432 'selected_tickets': '1,2',
433 })
434
435 bmm = BatchModifyModule(self.env)
436 self.assertRaises(RequestDone, bmm.process_request, req)
437 self.assertFieldChanged(1, 'reporter', 'user2')
438 self.assertFieldChanged(2, 'reporter', 'user2')
439
441 """User without TICKET_ADMIN cannot batch modify the reporter."""
442 self._insert_ticket('Ticket 1', reporter='user1')
443 self._insert_ticket('Ticket 2', reporter='user1')
444 req = MockRequest(self.env, method='POST', authname='has_bm', args={
445 'batchmod_value_reporter': 'user2',
446 'batchmod_value_comment': '',
447 'action': 'leave',
448 'selected_tickets': '1,2',
449 })
450
451 bmm = BatchModifyModule(self.env)
452 self.assertRaises(RequestDone, bmm.process_request, req)
453 self.assertFieldChanged(1, 'reporter', 'user1')
454 self.assertFieldChanged(2, 'reporter', 'user1')
455
457 """Template data added by post_process_request."""
458 self._insert_ticket("Ticket 1", status='new')
459 self._insert_ticket("Ticket 2", status='new')
460 req = MockRequest(self.env, path_info='/query')
461 req.session['query_href'] = '/query?status=!closed'
462 batch = BatchModifyModule(self.env)
463 data_in = {'tickets': [{'id': 1}, {'id': 2}]}
464
465 data_out = batch.post_process_request(req, 'query.html', data_in,
466 'text/html')[1]
467
468 self.assertTrue(data_out['batch_modify'])
469 self.assertEqual(['leave', 'resolve', 'reassign', 'accept'],
470 [a[0] for a in data_out['action_controls']])
471
473 """Actions added by custom ticket action controller.
474
475 Regression test for #12938.
476 """
477 class TestOperation(Component):
478 """TicketActionController that directly provides an action."""
479 implements(api.ITicketActionController)
480
481 def get_ticket_actions(self, req, ticket):
482 return [(0, 'test')]
483
484 def get_all_status(self):
485 return []
486
487 def render_ticket_action_control(self, req, ticket, action):
488 return "test", '', "This is a null action."
489
490 def get_ticket_changes(self, req, ticket, action):
491 return {}
492
493 def apply_action_side_effects(self, req, ticket, action):
494 pass
495
496 self._insert_ticket("Ticket 1", status='new')
497 self._insert_ticket("Ticket 2", status='new')
498 req = MockRequest(self.env, path_info='/query')
499 req.session['query_href'] = '/query?status=!closed'
500 batch = BatchModifyModule(self.env)
501 data_in = {'tickets': [{'id': 1}, {'id': 2}]}
502 self.env.config.set('ticket', 'workflow',
503 'ConfigurableTicketWorkflow, TestOperation')
504 self.env.enable_component(TestOperation)
505
506 data_out = batch.post_process_request(req, 'query.html', data_in,
507 'text/html')[1]
508
509 self.assertEqual(['leave', 'test', 'resolve', 'reassign', 'accept'],
510 [a[0] for a in data_out['action_controls']])
511
513 """Exception not raised in post_process_request error handling.
514 """
515 req = MockRequest(self.env, path_info='/query')
516 batch = BatchModifyModule(self.env)
517 self.assertEqual((None, None, None),
518 batch.post_process_request(req, None, None, None))
519
520
526
527
528 if __name__ == '__main__':
529 unittest.main(defaultTest='test_suite')
530