def check_ticket_access(self, perm, res): """Return if this req is permitted access to the given ticket ID.""" try: tkt = Ticket(self.env, res.id) except TracError: return None # Ticket doesn't exist had_any = False if perm.has_permission('TICKET_REPORTER'): had_any = True if tkt['reporter'] == perm.username: return None if perm.has_permission('TICKET_CC'): had_any = True cc_list = Chrome(self.env).cc_list(tkt['cc']) if perm.username in cc_list: return None if perm.has_permission('TICKET_OWNER'): had_any = True if perm.username == tkt['owner']: return None if perm.has_permission('TICKET_REPORTER_GROUP'): had_any = True if self._check_group(perm.username, tkt['reporter']): return None if perm.has_permission('TICKET_OWNER_GROUP'): had_any = True if self._check_group(perm.username, tkt['owner']): return None if perm.has_permission('TICKET_CC_GROUP'): had_any = True cc_list = Chrome(self.env).cc_list(tkt['cc']) for user in cc_list: #self.log.debug('Private: CC check: %s, %s', req.authname, user.strip()) if self._check_group(perm.username, user): return None # No permissions assigned, punt if not had_any: return None return False
def test_upgrade_v1_to_current(self): # The initial db schema from r2963 - 02-Jan-2008 by Alec Thomas. schema = [ Table('votes', key=('resource', 'username', 'vote'))[Column('resource'), Column('username'), Column('vote', 'int'), ] ] self._schema_init(schema) # Populate tables with test data. cursor = self.db.cursor() cursor.executemany( """ INSERT INTO votes (resource,username,vote) VALUES (%s,%s,%s) """, (('ticket/1', 'user', -1), ('ticket/2', 'user', 1), ('wiki/DeletedPage', 'user', -1), ('wiki/ExistingPage', 'user', 1))) # Resources must exist for successful data migration. t = Ticket(self.env, db=self.db) t['summary'] = 'test ticket' t.insert() w = WikiPage(self.env, 'ExistingPage') w.text = 'content' w.save('author', 'comment', '::1') self._verify_schema_unregistered() self.assertEquals(1, self.votes.get_schema_version(self.db)) self.assertTrue(self.votes.environment_needs_upgrade(self.db)) # Data migration and registration of unversioned schema. self.votes.upgrade_environment(self.db) self._verify_curr_schema() cursor.execute('SELECT * FROM votes') votes = cursor.fetchall() t_votes = [ id for realm, id, ver, u, v, t, c in votes if realm == 'ticket' ] w_votes = [ id for realm, id, ver, u, v, t, c in votes if realm == 'wiki' ] self.assertTrue('1' in t_votes) if resource_check: self.assertFalse('2' in t_votes) self.assertFalse('DeletedPage' in w_votes) self.assertTrue('ExistingPage' in w_votes)
def _format_comment_link(self, formatter, ns, target, label): resource = None if ':' in target: elts = target.split(':') if len(elts) == 3: cnum, realm, id = elts if cnum != 'description' and cnum and not cnum[0].isdigit(): realm, id, cnum = elts # support old comment: style id = as_int(id, None) resource = formatter.resource(realm, id) else: resource = formatter.resource cnum = target if resource and resource.id and resource.realm == self.realm and \ cnum and (all(c.isdigit() for c in cnum) or cnum == 'description'): href = title = class_ = None if self.resource_exists(resource): from trac.ticket.model import Ticket ticket = Ticket(self.env, resource.id) if cnum != 'description' and not ticket.get_change(cnum): title = _("ticket comment does not exist") class_ = 'missing ticket' elif 'TICKET_VIEW' in formatter.perm(resource): href = formatter.href.ticket(resource.id) + \ "#comment:%s" % cnum if resource.id != formatter.resource.id: if cnum == 'description': title = _("Description for Ticket #%(id)s", id=resource.id) else: title = _("Comment %(cnum)s for Ticket #%(id)s", cnum=cnum, id=resource.id) class_ = ticket['status'] + ' ticket' else: title = _("Description") if cnum == 'description' \ else _("Comment %(cnum)s", cnum=cnum) class_ = 'ticket' else: title = _("no permission to view ticket") class_ = 'forbidden ticket' else: title = _("ticket does not exist") class_ = 'missing ticket' return tag.a(label, class_=class_, href=href, title=title) return label
def __init__(self, env, tkt, db=None): self.env = env if not isinstance(tkt, Ticket): tkt = Ticket(self.env, tkt) self.tkt = tkt db = db or self.env.get_db_cnx() cursor = db.cursor() cursor.execute('SELECT dest FROM mastertickets WHERE source=%s ORDER BY dest', (self.tkt.id,)) self.blocking = set([int(num) for num, in cursor]) self._old_blocking = copy.copy(self.blocking) cursor.execute('SELECT source FROM mastertickets WHERE dest=%s ORDER BY source', (self.tkt.id,)) self.blocked_by = set([int(num) for num, in cursor]) self._old_blocked_by = copy.copy(self.blocked_by)
def test_owner_from_component(self): """Verify that the owner of a new ticket is set to the owner of the component. """ self._add_component('component3', 'cowner3') req = self._create_request(method='POST', args={ 'field_reporter': 'reporter1', 'field_summary': 'the summary', 'field_component': 'component3', }) self.assertRaises(RequestDone, self.ticket_module.process_request, req) ticket = Ticket(self.env, 1) self.assertEqual('component3', ticket['component']) self.assertEqual('cowner3', ticket['owner'])
def render_widget(self, name, context, options): """Gather list of relations and render data in compact view """ req = context.req title = _('Related tickets') params = ('tid', 'max') tid, max_ = self.bind_params(name, options, *params) ticket = Ticket(self.env, tid) data = { 'ticket': ticket, 'relations': \ RelationManagementModule(self.env).get_ticket_relations(ticket), } return 'widget_relations.html', \ {'title': title, 'data': data, }, context
def test_owner_from_component(self): """ Verify that the owner of a new ticket is set to the owner of the component. """ component = Component(self.env) component.name = 'test' component.owner = 'joe' component.insert() ticket = Ticket(self.env) ticket['reporter'] = 'santa' ticket['summary'] = 'Foo' ticket['component'] = 'test' ticket.insert() self.assertEqual('joe', ticket['owner'])
def _link(resource): if resource.realm == 'tag': # Keep realm selection in tag links. return builder.a(resource.id, href=self.get_href(req, realms, tag=resource)) elif resource.realm == 'ticket': # Return resource link including ticket status dependend # class to allow for common Trac ticket link style. ticket = Ticket(env, resource.id) return builder.a('#%s' % ticket.id, class_=ticket['status'], href=formatter.href.ticket(ticket.id), title=shorten_line(ticket['summary'])) return render_resource_link(env, context, resource, 'compact')
def linkify_ids(env, req, ids): data = [] for id in sorted(ids, key=lambda x: int(x)): try: tkt = Ticket(env, id) data.append( tag.a('#%s' % tkt.id, href=req.href.ticket(tkt.id), class_='%s ticket' % tkt['status'], title=tkt['summary'])) except ResourceNotFound: data.append('#%s' % id) data.append(', ') if data: del data[-1] # Remove the last comma if needed return tag.span(*data)
def removeBlockedTicket( self, ticket_id, old_blockedtid): ''' remove ticket_id from the dependencies of old_blockedtid ''' dependencies = self.getDependsOn(old_blockedtid) dependencies_list = self.splitStringToTicketList(dependencies) new_dependencies_list = [ t.strip() for t in dependencies_list if str(t).strip() != ticket_id ] new_dependencies = self.createNormalizedTicketString(new_dependencies_list) self.saveDependenciesToDatabase( old_blockedtid, new_dependencies ) comment = 'note: change "'+dependencies+'" to "'+new_dependencies+'" (remove '+str(ticket_id)+') initiated by #'+str(ticket_id) try: Ticket(self.env, old_blockedtid).save_changes(self.authname, comment ) # add comment to ticket self.env.log.error('consider #%s: change dependencies of #%s: %s --> %s' % (ticket_id, old_blockedtid, dependencies, new_dependencies) ) except Exception,e: self.env.log.error('error while adding the comment "%s" to #%s: %s' % (comment,ticket_id,repr(e)) )
def _test_template_data_for_time_field(self, req, value, expected, format): self.env.config.set('ticket-custom', 'timefield', 'time') if format: self.env.config.set('ticket-custom', 'timefield.format', format) self._insert_ticket(summary='Time fields', timefield=value) self.assertEqual(value, Ticket(self.env, 1)['timefield']) self.assertTrue(self.ticket_module.match_request(req)) data = self.ticket_module.process_request(req)[1] for f in data['fields']: if f['name'] == 'timefield': self.assertEqual(expected, f['edit']) break else: self.fail('Missing timefield field')
def test_user_can_edit_ticket_cc_for_new_ticket(self): """User without TICKET_EDIT_CC can edit CC field for new ticket.""" action = 'TICKET_EDIT_CC' ticket1 = Ticket(self.env) ticket2 = self._insert_ticket('somebody1') perm_cache1 = PermissionCache(self.env, 'somebody1', ticket1.resource) self.assertIn(action, perm_cache1) self.assertTrue(self.policy.check_permission( action, perm_cache1.username, ticket1.resource, perm_cache1)) # No decision for existing ticket. perm_cache2 = PermissionCache(self.env, 'somebody1', ticket2.resource) self.assertNotIn(action, perm_cache2) self.assertIsNone(self.policy.check_permission( action, perm_cache2.username, ticket2.resource, perm_cache2))
def test_no_recipient(self): """No recipient case""" self.env.config.set('notification', 'smtp_always_cc', '') ticket = Ticket(self.env) ticket['reporter'] = 'anonymous' ticket['summary'] = 'Foo' ticket.insert() tn = TicketNotifyEmail(self.env) tn.notify(ticket, newticket=True) sender = notifysuite.smtpd.get_sender() recipients = notifysuite.smtpd.get_recipients() message = notifysuite.smtpd.get_message() # checks that no message has been sent self.failIf(recipients) self.failIf(sender) self.failIf(message)
def test_multiline_header(self): """Encoded headers split into multiple lines""" self.env.config.set('notification', 'mime_encoding', 'qp') ticket = Ticket(self.env) ticket['reporter'] = '*****@*****.**' # Forces non-ascii characters ticket['summary'] = u'A_very %s súmmäry' % u' '.join(['long'] * 20) ticket.insert() tn = TicketNotifyEmail(self.env) tn.notify(ticket, newticket=True) message = notifysuite.smtpd.get_message() (headers, body) = parse_smtp_message(message) # Discards the project name & ticket number subject = headers['Subject'] summary = subject[subject.find(':') + 2:] self.failIf(ticket['summary'] != summary)
def _setup_env_and_req(self, max_size, field_value): self.env.config.set('ticket-custom', 'text1', 'text') self.env.config.set('ticket-custom', 'text1.max_size', max_size) ticket = insert_ticket(self.env, summary='summary', text1='init') change_time = Ticket(self.env, ticket.id)['changetime'] view_time = str(to_utimestamp(change_time)) req = MockRequest(self.env, method='POST', path_info='/ticket/%d' % ticket.id, args={ 'submit': 'Submit changes', 'field_text1': field_value, 'action': 'leave', 'view_time': view_time }) return req
def test_modify_missing_cnums_and_comment(self): """Editing a comments when all cnums are missing and one comment field is missing """ cursor = self.db.cursor() cursor.execute("UPDATE ticket_change SET oldvalue='' " "WHERE oldvalue='1'") cursor.execute("DELETE FROM ticket_change " "WHERE field='comment' AND oldvalue='1.2'") cursor.execute("UPDATE ticket_change SET oldvalue='' " "WHERE oldvalue='3'") self.db.commit() # Modify after missing comment ticket = Ticket(self.env, self.id) t = self.created + timedelta(seconds=50) ticket.modify_comment(self._find_change(ticket, 3), 'joe', 'New comment 3', t) self.assertChange(ticket, 3, self.t3, 'jim', keywords=dict(author='jim', old='a, b, c', new='a, b'), comment=dict(author='jim', old='', new='New comment 3'), _comment0=dict(author='joe', old='Comment 3', new=str(to_utimestamp(t)))) # Modify missing comment t = self.created + timedelta(seconds=60) ticket.modify_comment(self._find_change(ticket, 2), 'joe', 'New comment 2', t) self.assertChange(ticket, 2, self.t2, 'john', owner=dict(author='john', old='john', new='jack'), comment=dict(author='john', old='', new='New comment 2'), _comment0=dict(author='joe', old='', new=str(to_utimestamp(t))))
def test_props_format_wrap_bothsides(self): self.env.config.set('notification', 'mime_encoding', 'none') ticket = Ticket(self.env) ticket['summary'] = u'This is a summary' ticket['reporter'] = u'anonymous' ticket['status'] = u'new' ticket['owner'] = u'somebody' ticket['type'] = u'defect' ticket['priority'] = u'major' ticket['milestone'] = u'Lorem ipsum dolor sit amet, consectetur ' \ u'adipisicing elit, sed do eiusmod tempor ' \ u'incididunt ut labore et dolore magna ' \ u'aliqua. Ut enim ad minim veniam, quis ' \ u'nostrud exercitation ullamco laboris nisi ' \ u'ut aliquip ex ea commodo consequat. Duis ' \ u'aute irure dolor in reprehenderit in ' \ u'voluptate velit esse cillum dolore eu ' \ u'fugiat nulla pariatur. Excepteur sint ' \ u'occaecat cupidatat non proident, sunt in ' \ u'culpa qui officia deserunt mollit anim id ' \ u'est laborum.' ticket['component'] = ticket['milestone'] ticket['version'] = u'2.0' ticket['resolution'] = u'fixed' ticket['keywords'] = u'' ticket.insert() formatted = """\ Reporter: anonymous | Owner: somebody Type: defect | Status: new Priority: major | Milestone: Lorem ipsum dolor sit Component: Lorem ipsum dolor sit | amet, consectetur adipisicing elit, amet, consectetur adipisicing | sed do eiusmod tempor incididunt ut elit, sed do eiusmod tempor | labore et dolore magna aliqua. Ut incididunt ut labore et dolore | enim ad minim veniam, quis nostrud magna aliqua. Ut enim ad minim | exercitation ullamco laboris nisi veniam, quis nostrud exercitation | ut aliquip ex ea commodo consequat. ullamco laboris nisi ut aliquip | Duis aute irure dolor in ex ea commodo consequat. Duis | reprehenderit in voluptate velit aute irure dolor in reprehenderit | esse cillum dolore eu fugiat nulla in voluptate velit esse cillum | pariatur. Excepteur sint occaecat dolore eu fugiat nulla pariatur. | cupidatat non proident, sunt in Excepteur sint occaecat cupidatat | culpa qui officia deserunt mollit non proident, sunt in culpa qui | anim id est laborum. officia deserunt mollit anim id | Version: 2.0 est laborum. | Keywords: Resolution: fixed |""" self._validate_props_format(formatted, ticket)
def display_ticket_page(self, req, ticketId): # This method is based on process_request() in TicketModule. # todo: security check should go here # --- For security, only display ticket if it's req.perm.assert_permission('TICKET_VIEW') action = req.args.get('action', 'view') db = self.env.get_db_cnx() ticket = Ticket(self.env, ticketId, db=db) reporter_id = req.args.get('author') req.hdf['ticket.debug'] = self.debug if req.method == 'POST': if not req.args.has_key('preview'): self.save_ticket_form_data(req, db, ticket) else: # Use user supplied values ticket.populate(req.args) req.hdf['ticket.action'] = action req.hdf['ticket.ts'] = req.args.get('ts') req.hdf['ticket.reassign_owner'] = req.args.get('reassign_owner') \ or req.authname req.hdf['ticket.resolve_resolution'] = req.args.get('resolve_resolution') reporter_id = req.args.get('author') comment = req.args.get('comment') if comment: req.hdf['ticket.comment'] = comment # Wiki format a preview of comment req.hdf['ticket.comment_preview'] = wiki_to_html(comment, self.env, req, db) else: req.hdf['ticket.reassign_owner'] = req.authname # Store a timestamp in order to detect "mid air collisions" req.hdf['ticket.ts'] = ticket.time_changed self.insert_ticket_data_to_hdf(req, db, ticket) add_stylesheet(req, 'common/css/ticket.css') return 'autotracticket.cs', None
def pre_process_request(self, req, handler): def createDiff( ticket_id ): ''' HACK: need an indicator when the ticket was created lately ''' diff = -1 if ticket_id != None: created = Ticket(self.env, int(ticket_id)).time_created now = datetime.now(created.tzinfo) # needs tzinfo! created = datetime.combine( created.date(), created.time() ) now = datetime.combine( now.date(), now.time() ) diff = now - created diff = diff.seconds + diff.days * 24 * 3600 # calculated here, to be compatible to python <2.7 return diff if req.path_info.startswith('/ticket/') or req.path_info.startswith('/newticket'): self.lazy_init(req.authname) # get blocked tickets and save the dependencies blocked_tickets = req.args.get('field_'+self.fieldrev) blocked_tickets_backup = req.args.get('field_%s_backup' % (self.fieldrev) ) blocking_tickets = req.args.get('field_'+self.field) blocking_tickets_backup = req.args.get('field_%s_backup' % (self.field) ) ticket_id = req.args.get('id') newTicketFlag = False if (0 <= createDiff( ticket_id ) <= 1) and req.path_info.startswith('/ticket') : blocked_tickets = Ticket(self.env, int(ticket_id)).get_value_or_default(self.fieldrev) # read from the DB self.env.log.debug('pre_process_request: new ticket #%s created: %s --> save blocked tickets' % (ticket_id, blocked_tickets)) # hack newTicketFlag = True # save "blocks tickets": save if the field was provided and changed, moreover, the submit button was hit if (blocked_tickets != None and req.path_info.startswith('/ticket') and req.args.get('submit') != "" and req.args.get('submit') != None) or newTicketFlag: if blocked_tickets != blocked_tickets_backup: self.env.log.debug('pre_process_request: ticket #%s change_blocked_tickets %s' % (ticket_id, blocked_tickets)) # hack self.authname = req.authname #blocked_tickets = Ticket(self.env, int(ticket_id)).get_value_or_default(self.fieldrev) self.change_blocked_tickets( ticket_id , blocked_tickets ) if blocking_tickets != blocking_tickets_backup: self.change_blocking_tickets( ticket_id, blocking_tickets ) self.cleanUp(req) #if blocked_tickets != None : self.authname = req.authname return handler
def test_transition_to_star(self): """Action not rendered by CTW for transition to * AdvancedTicketWorkflow uses the behavior for the triage operation (see #12823) """ config = self.env.config config.set('ticket-workflow', 'create_and_triage', '<none> -> *') config.set('ticket-workflow', 'create_and_triage.operations', 'triage') self._reload_workflow() ticket = Ticket(self.env) req = MockRequest(self.env, path_info='/newticket', method='POST') actions = self.ctlr.get_ticket_actions(req, ticket) # create_and_triage not in actions self.assertEqual([(1, 'create'), (0, 'create_and_assign')], actions)
def macro_setup(tc): tc.env = EnvironmentStub(enable=( 'trac.*', 'tracopt.ticket.commit_updater.*', )) ticket = Ticket(tc.env) ticket['summary'] = 'the summary' ticket['status'] = 'new' ticket.insert() def _get_repository(reponame): return Mock(get_changeset=_get_changeset, resource=None) def _get_changeset(rev=None): return Mock(message="the message. refs #1. ", rev=rev) setattr(RepositoryManager(tc.env), 'get_repository', _get_repository)
def test_threading(self): """Check modification of a "threaded" comment""" ticket = Ticket(self.env, self.id) t = self.created + timedelta(seconds=20) ticket.modify_comment(self._find_change(ticket, 2), 'joe', 'New comment 2', t) self.assertChange(ticket, 2, self.t2, 'john', owner=dict(author='john', old='john', new='jack'), comment=dict(author='john', old='1.2', new='New comment 2'), _comment0=dict(author='joe', old='Comment 2', new=str(to_utimestamp(t))))
def check_ticket_permissions(self, action, perm, res): """Return if this req is generating permissions for the given ticket ID.""" try: tkt = Ticket(self.env, res.id) except TracError: return None # Ticket doesn't exist if action == 'TICKET_IS_SELF': return tkt['reporter'] == perm.username or \ perm.username == tkt['owner'] or \ perm.username in [x.strip() for x in tkt['cc'].split(',')] if action == 'TICKET_IS_REPORTER': return tkt['reporter'] == perm.username if action == 'TICKET_IS_CC': return perm.username in [x.strip() for x in tkt['cc'].split(',')] if action == 'TICKET_IS_OWNER': return perm.username == tkt['owner'] if action == 'TICKET_IS_GROUP': result = self._check_group(perm.username, tkt['reporter']) or \ self._check_group(perm.username, tkt['owner']) for user in tkt['cc'].split(','): #self.log.debug('Private: CC check: %s, %s', req.authname, user.strip()) if self._check_group(perm.username, user.strip()): result = True return result if action == 'TICKET_IS_REPORTER_GROUP': return self._check_group(perm.username, tkt['reporter']) if action == 'TICKET_IS_OWNER_GROUP': return self._check_group(perm.username, tkt['owner']) if action == 'TICKET_IS_CC_GROUP': result = False for user in tkt['cc'].split(','): #self.log.debug('Private: CC check: %s, %s', req.authname, user.strip()) if self._check_group(perm.username, user.strip()): result = True return result # We should never get here return None
def _append_parent_links(self, req, data, ids): links = [] for id in sorted(ids, key=lambda x: int(x)): try: ticket = Ticket(self.env, id) elem = tag.a('#%s' % id, href=req.href.ticket(id), class_='%s ticket' % ticket['status'], title=ticket['summary']) if len(links) > 0: links.append(', ') links.append(elem) except ResourceNotFound: pass for field in data.get('fields', ''): if field.get('name') == 'parents': field['rendered'] = tag.span(*links)
def remove_resource_tags(self, req, ticket_or_resource, comment=u''): try: resource = ticket_or_resource.resource except AttributeError: resource = ticket_or_resource assert resource.realm == self.realm if not self._check_permission(req, resource, 'modify'): raise PermissionError(resource=resource, env=self.env) # Processing a call from TracTags, try to alter the ticket. ticket = Ticket(self.env, resource.id) # Can only alter tags in 'keywords' ticket field. # DEVEL: Time to differentiate managed and sticky/unmanaged tags? ticket['keywords'] = u'' ticket.save_changes(get_reporter_id(req), comment) else: # Processing a change listener event. super(TicketTagProvider, self).remove_resource_tags(req, resource)
def _notify_attachment(self, attachment, category, time): resource = attachment.resource.parent if resource.realm != 'ticket': return ticket = Ticket(self.env, resource.id) event = TicketChangeEvent(category, ticket, time, ticket['reporter'], attachment=attachment) try: NotificationSystem(self.env).notify(event) except Exception as e: self.log.error( "Failure sending notification when adding " "attachment %s to ticket #%s: %s", attachment.filename, ticket.id, exception_to_unicode(e))
def _validate(self, req, arg): """Validate that arg is a string containing a valid ticket ID.""" if not arg: add_warning(req, "Ticket ID was not entered.") return False try: id = int(arg.lstrip('#')) t = Ticket(self.env, id) return t except TracError: if id > 0: add_warning(req, "Ticket #%s does not exist." % id) else: add_warning(req, "'%s' is not a valid ticket ID." % id) except ValueError: add_warning(req, "'%s' is not a valid ticket ID." % arg) return False
def test_change_listener_changed(self): listener = TestTicketChangeListener(self.env) data = {'component': 'foo', 'milestone': 'bar'} tkt_id = self._insert_ticket('Hello World', reporter='john', **data) ticket = Ticket(self.env, tkt_id) ticket['component'] = 'new component' ticket['milestone'] = 'new milestone' comment = 'changing ticket' ticket.save_changes('author', comment) self.assertEqual('changed', listener.action) self.assertEqual(comment, listener.comment) self.assertEqual('author', listener.author) for key, value in data.iteritems(): self.assertEqual(value, listener.old_values[key])
def get_ticket_changes(self, req, ticket, action): id = 'vote_%s_result' % (action, ) selected = req.args.get(id, 'for') priorities = list(Priority.select(self.env)) orig_ticket = Ticket(self.env, ticket.id) current_priority = int( Priority(self.env, name=orig_ticket['priority']).value) if selected == 'for': # priorities are 1-based, not 0-based new_value = max(1, current_priority - 1) else: maxval = max([int(p.value) for p in priorities]) new_value = min(maxval, current_priority + 1) return { 'priority': [p.name for p in priorities if int(p.value) == new_value][0] }
def test_status_change_with_no_operation(self): """Existing ticket status change with no operation.""" config = self.env.config config.set('ticket-workflow', 'change_status', 'status1 -> status2') self._reload_workflow() ticket = Ticket(self.env) ticket['status'] = 'status1' ticket.insert() req = MockRequest(self.env, path_info='/ticket', method='POST') label, control, hints = \ self.ctlr.render_ticket_action_control(req, ticket, 'change_status') self.assertEqual('change status', label) self.assertEqual('', unicode(control)) self.assertEqual("Next status will be 'status2'.", unicode(hints))