Package trac :: Package ticket :: Package tests :: Module batch

Source Code for Module trac.ticket.tests.batch

  1  # -*- coding: utf-8 -*- 
  2  # 
  3  # Copyright (C) 2012-2020 Edgewall Software 
  4  # All rights reserved. 
  5  # 
  6  # This software is licensed as described in the file COPYING, which 
  7  # you should have received as part of this distribution. The terms 
  8  # are also available at https://trac.edgewall.org/wiki/TracLicense. 
  9  # 
 10  # This software consists of voluntary contributions made by many 
 11  # individuals. For the exact contribution history, see the revision 
 12  # history and logs, available at https://trac.edgewall.org/log/. 
 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   
30 -class BatchModifyTestCase(unittest.TestCase):
31
32 - def setUp(self):
33 self.env = EnvironmentStub(default_data=True, 34 enable=[default_workflow.ConfigurableTicketWorkflow, 35 DefaultPermissionPolicy, DefaultPermissionStore, 36 web_ui.TicketModule]) 37 self.env.config.set('trac', 'permission_policies', 38 'DefaultPermissionPolicy') 39 self.req = MockRequest(self.env, authname='anonymous', 40 path_info='/query')
41
42 - def assertCommentAdded(self, ticket_id, comment):
43 ticket = Ticket(self.env, int(ticket_id)) 44 changes = ticket.get_changelog() 45 comment_change = [c for c in changes if c[2] == 'comment'][-1] 46 self.assertEqual(comment_change[2], comment)
47
48 - def assertFieldChanged(self, ticket_id, field, new_value):
49 ticket = Ticket(self.env, int(ticket_id)) 50 changes = ticket.get_changelog() 51 field_change = [c for c in changes if c[2] == field][-1] 52 self.assertEqual(field_change[4], new_value)
53
54 - def _change_list_test_helper(self, original, new, new2, mode):
55 batch = BatchModifyModule(self.env) 56 return batch._change_list(original, new, new2, mode)
57
58 - def _add_list_test_helper(self, original, to_add):
59 return self._change_list_test_helper(original, to_add, '', '+')
60
61 - def _remove_list_test_helper(self, original, to_remove):
62 return self._change_list_test_helper(original, to_remove, '', '-')
63
64 - def _add_remove_list_test_helper(self, original, to_add, to_remove):
65 return self._change_list_test_helper(original, to_add, to_remove, 66 '+-')
67
68 - def _assign_list_test_helper(self, original, new):
69 return self._change_list_test_helper(original, new, '', '=')
70
71 - def _insert_ticket(self, summary, **kw):
72 """Helper for inserting a ticket into the database""" 73 ticket = Ticket(self.env) 74 for k, v in kw.items(): 75 ticket[k] = v 76 return ticket.insert()
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
91 batch = BatchModifyModule(self.env) 92 self.req.args = {'batchmod_value_milestone': 'milestone1'} 93 values = batch._get_new_ticket_values(self.req) 94 self.assertEqual(values['milestone'], 'milestone1')
95
96 - def test_selected_tickets(self):
97 self.req.args = {'selected_tickets': '1,2,3'} 98 batch = BatchModifyModule(self.env) 99 selected_tickets = batch._get_selected_tickets(self.req) 100 self.assertEqual(selected_tickets, ['1', '2', '3'])
101
102 - def test_no_selected_tickets(self):
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
109 - def test_require_post_method(self):
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 # Assign list items 141
142 - def test_change_list_replace_empty_with_single(self):
143 """Replace empty field with single item.""" 144 changed = self._assign_list_test_helper('', 'alice') 145 self.assertEqual(changed, 'alice')
146
147 - def test_change_list_replace_empty_with_items(self):
148 """Replace empty field with items.""" 149 changed = self._assign_list_test_helper('', 'alice, bob') 150 self.assertEqual(changed, 'alice, bob')
151
152 - def test_change_list_replace_item(self):
153 """Replace item with a different item.""" 154 changed = self._assign_list_test_helper('alice', 'bob') 155 self.assertEqual(changed, 'bob')
156
157 - def test_change_list_replace_item_with_items(self):
158 """Replace item with different items.""" 159 changed = self._assign_list_test_helper('alice', 'bob, carol') 160 self.assertEqual(changed, 'bob, carol')
161
162 - def test_change_list_replace_items_with_item(self):
163 """Replace items with a different item.""" 164 changed = self._assign_list_test_helper('alice, bob', 'carol') 165 self.assertEqual(changed, 'carol')
166
167 - def test_change_list_replace_items(self):
168 """Replace items with different items.""" 169 changed = self._assign_list_test_helper('alice, bob', 'carol, dave') 170 self.assertEqual(changed, 'carol, dave')
171
172 - def test_change_list_replace_items_partial(self):
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
177 - def test_change_list_clear(self):
178 """Clear field.""" 179 changed = self._assign_list_test_helper('alice bob', '') 180 self.assertEqual(changed, '')
181 182 # Add / remove list items 183
184 - def test_change_list_add_item(self):
185 """Append additional item.""" 186 changed = self._add_list_test_helper('alice', 'bob') 187 self.assertEqual(changed, 'alice, bob')
188
189 - def test_change_list_add_items(self):
190 """Append additional items.""" 191 changed = self._add_list_test_helper('alice, bob', 'carol, dave') 192 self.assertEqual(changed, 'alice, bob, carol, dave')
193
194 - def test_change_list_remove_item(self):
195 """Remove existing item.""" 196 changed = self._remove_list_test_helper('alice, bob', 'bob') 197 self.assertEqual(changed, 'alice')
198
199 - def test_change_list_remove_items(self):
200 """Remove existing items.""" 201 changed = self._remove_list_test_helper('alice, bob, carol', 202 'alice, carol') 203 self.assertEqual(changed, 'bob')
204
205 - def test_change_list_remove_idempotent(self):
206 """Ignore missing item to be removed.""" 207 changed = self._remove_list_test_helper('alice', 'bob') 208 self.assertEqual(changed, 'alice')
209
210 - def test_change_list_remove_mixed(self):
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
215 - def test_change_list_add_remove(self):
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
221 - def test_change_list_add_no_duplicates(self):
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
226 - def test_change_list_remove_all_duplicates(self):
227 """Remove all duplicates.""" 228 changed = self._remove_list_test_helper('alice, bob, alice', 'alice') 229 self.assertEqual(changed, 'bob')
230 231 # Save 232
233 - def test_save_comment(self):
234 """Comments are saved to all selected tickets.""" 235 first_ticket_id = self._insert_ticket('Test 1', reporter='joe') 236 second_ticket_id = self._insert_ticket('Test 2', reporter='joe') 237 selected_tickets = [first_ticket_id, second_ticket_id] 238 239 batch = BatchModifyModule(self.env) 240 batch._save_ticket_changes(self.req, selected_tickets, {}, 'comment', 241 'leave') 242 243 self.assertCommentAdded(first_ticket_id, 'comment') 244 self.assertCommentAdded(second_ticket_id, 'comment')
245
246 - def test_save_values(self):
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
261 - def test_save_list_fields(self):
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': '+', # add 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': '+-', # add / remove 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': '-', # remove 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': '=', # set 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
303 - def test_action_with_state_change(self):
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
319 - def test_action_with_side_effects(self):
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
338 - def test_timeline_events(self):
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 # shuffle ticket_change records 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
386 -class ProcessRequestTestCase(unittest.TestCase):
387
388 - def setUp(self):
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
407 - def tearDown(self):
408 self.env.reset_db()
409
410 - def assertFieldChanged(self, ticket_id, field, new_value):
411 ticket = Ticket(self.env, int(ticket_id)) 412 self.assertEqual(ticket[field], new_value)
413
414 - def _insert_ticket(self, summary, **kw):
415 """Helper for inserting a ticket into the database""" 416 ticket = Ticket(self.env) 417 ticket['summary'] = summary 418 for k, v in kw.items(): 419 ticket[k] = v 420 return ticket.insert()
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
456 - def test_post_process_request_add_template_data(self):
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
512 - def test_post_process_request_error_handling(self):
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
521 -def test_suite():
522 suite = unittest.TestSuite() 523 suite.addTest(unittest.makeSuite(BatchModifyTestCase)) 524 suite.addTest(unittest.makeSuite(ProcessRequestTestCase)) 525 return suite
526 527 528 if __name__ == '__main__': 529 unittest.main(defaultTest='test_suite') 530