def print_edit_view(self, req, error=None, filter=None, warning=None): userMail = self.get_user_email(req.authname) fields = TicketSystem(self.env).get_ticket_fields() customFields = TicketSystem(self.env).get_custom_fields() for f in fields[:]: for cf in customFields: if f['name'] == cf['name']: fields.remove(f) disableSubmitButton = "" if not userMail: userMail = ["No email specified"] disableSubmitButton = "disabled" data = {'userEmail' : userMail, 'submitDisabled': disableSubmitButton, 'datetime_hint': get_datetime_format_hint(), 'fields': fields, 'filter': filter, 'error': error, 'warning': warning} add_stylesheet(req, 'hw/css/style.css') add_script(req, 'hw/js/xmail.js') return 'xmail-edit.html', data, None
def ticket_created(self, ticket): ticketsystem = TicketSystem(self.env) resource_name = get_resource_shortname(self.env, ticket.resource) resource_desc = ticketsystem.get_resource_description(ticket.resource, format='summary') # make sure we will index customerrequest name not id cr_id = ticket['customerrequest'] if cr_id: db = self.env.get_read_db() cursor = db.cursor() cursor.execute("SELECT name FROM public.customer_requests " " WHERE id='%s'" % cr_id) row = cursor.fetchone() if row: ticket.values['customerrequest'] = row[0] so = FullTextSearchObject( self.project, ticket.resource, title = u"%(title)s: %(message)s" % {'title': resource_name, 'message': resource_desc}, author = ticket.values.get('reporter'), changed = ticket.values.get('changetime'), created = ticket.values.get('time'), tags = ticket.values.get('keywords'), involved = re.split(r'[;,\s]+', ticket.values.get('cc', '')) or ticket.values.get('reporter'), popularity = 0, #FIXME oneline = shorten_result(ticket.values.get('description', '')), body = u'%r' % (ticket.values,), status = ticket.values.get('status'), comments = [t[4] for t in ticket.get_changelog()], ) self.backend.create(so, quiet=True) self._update_ticket(ticket) self.log.debug("Ticket added for indexing: %s", ticket)
def print_edit_view(self, req, error=None, filter=None, warning=None): userMail = self.get_user_email(req.authname) fields = TicketSystem(self.env).get_ticket_fields() customFields = TicketSystem(self.env).get_custom_fields() for f in fields[:]: for cf in customFields: if f["name"] == cf["name"]: fields.remove(f) disableSubmitButton = "" if not userMail: userMail = ["No email specified"] disableSubmitButton = "disabled" data = { "userEmail": userMail, "submitDisabled": disableSubmitButton, "datetime_hint": get_datetime_format_hint(), "fields": fields, "filter": filter, "error": error, "warning": warning, } add_stylesheet(req, "hw/css/style.css") add_script(req, "hw/js/xmail.js") return "xmail-edit.html", data, None
def __init__(self): self.log.debug('Starting Sqa Testing System API') ts = TicketSystem(self.env) ''' When the object is created collect a set of tickets configured in this running instance of track ''' self.ticket_fields = ts.get_ticket_fields()
def _check_field_existance(self): ticket_system = TicketSystem(self.env) custom_fields = ticket_system.get_custom_fields() for custom_field in custom_fields: if custom_field['type'] == 'text': if custom_field['name'] == self.tfield: return True return False
def describe_tagged_resource(self, req, resource): if not self.check_permission(req.perm, 'view'): return '' ticket = Ticket(self.env, resource.id) if ticket.exists: # Use the corresponding IResourceManager. ticket_system = TicketSystem(self.env) return ticket_system.get_resource_description(ticket.resource, format='summary') else: return ''
def test_available_actions_no_perms(self): ts = TicketSystem(self.env) perm = Mock(has_permission=lambda x: 0) self.assertEqual(['leave'], ts.get_available_actions({'status': 'new'}, perm)) self.assertEqual(['leave'], ts.get_available_actions({'status': 'assigned'}, perm)) self.assertEqual(['leave'], ts.get_available_actions({'status': 'reopened'}, perm)) self.assertEqual(['leave'], ts.get_available_actions({'status': 'closed'}, perm))
def test_available_actions_create_only(self): ts = TicketSystem(self.env) perm = Mock(has_permission=lambda x: x == 'TICKET_CREATE') self.assertEqual(['leave'], ts.get_available_actions({'status': 'new'}, perm)) self.assertEqual(['leave'], ts.get_available_actions({'status': 'assigned'}, perm)) self.assertEqual(['leave'], ts.get_available_actions({'status': 'reopened'}, perm)) self.assertEqual(['leave', 'reopen'], ts.get_available_actions({'status': 'closed'}, perm))
class ResetActionTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True) self.perm_sys = PermissionSystem(self.env) self.ctlr = TicketSystem(self.env).action_controllers[0] self.req1 = Mock(authname='user1', args={}, perm=PermissionCache(self.env, 'user1')) self.req2 = Mock(authname='user2', args={}, perm=PermissionCache(self.env, 'user2')) self.ticket = Ticket(self.env) self.ticket['status'] = 'invalid' self.ticket.insert() def tearDown(self): self.env.reset_db() def _reload_workflow(self): self.ctlr.actions = self.ctlr.get_all_actions() def test_default_reset_action(self): """Default reset action.""" self.perm_sys.grant_permission('user2', 'TICKET_ADMIN') self._reload_workflow() actions1 = self.ctlr.get_ticket_actions(self.req1, self.ticket) actions2 = self.ctlr.get_ticket_actions(self.req2, self.ticket) chgs2 = self.ctlr.get_ticket_changes(self.req2, self.ticket, '_reset') self.assertEqual(1, len(actions1)) self.assertNotIn((0, '_reset'), actions1) self.assertEqual(2, len(actions2)) self.assertIn((0, '_reset'), actions2) self.assertEqual('new', chgs2['status']) def test_custom_reset_action(self): """Custom reset action in [ticket-workflow] section.""" config = self.env.config['ticket-workflow'] config.set('_reset', '-> review') config.set('_reset.operations', 'reset_workflow') config.set('_reset.permissions', 'TICKET_BATCH_MODIFY') config.set('_reset.default', 2) self.perm_sys.grant_permission('user2', 'TICKET_BATCH_MODIFY') self._reload_workflow() actions1 = self.ctlr.get_ticket_actions(self.req1, self.ticket) actions2 = self.ctlr.get_ticket_actions(self.req2, self.ticket) chgs2 = self.ctlr.get_ticket_changes(self.req2, self.ticket, '_reset') self.assertEqual(1, len(actions1)) self.assertNotIn((2, '_reset'), actions1) self.assertEqual(2, len(actions2)) self.assertIn((2, '_reset'), actions2) self.assertEqual('review', chgs2['status'])
def _translation_deactivated(ticket=None): t = deactivate() if ticket is not None: ts = TicketSystem(ticket.env) translated_fields = ticket.fields ticket.fields = ts.get_ticket_fields() try: yield finally: if ticket is not None: ticket.fields = translated_fields reactivate(t)
def _complete_transition(self, args): if len(args) < 3: states = TicketSystem(self.env).get_all_status() if len(args) == 2 and args[0] in states: states.remove(args[0]) return states if len(args) == 3: return self.common_days if len(args) == 4: return self._get_user_list() if len(args) == 5: return self._get_explanations(args[3])
def test_available_actions_chgprop_only(self): # CHGPROP is not enough for changing a ticket's state (#3289) ts = TicketSystem(self.env) perm = Mock(has_permission=lambda x: x == 'TICKET_CHGPROP') self.assertEqual(['leave'], ts.get_available_actions({'status': 'new'}, perm)) self.assertEqual(['leave'], ts.get_available_actions({'status': 'assigned'}, perm)) self.assertEqual(['leave'], ts.get_available_actions({'status': 'reopened'}, perm)) self.assertEqual(['leave'], ts.get_available_actions({'status': 'closed'}, perm))
def get_search_results(self, req, terms, filters): """Overriding search results for Tickets""" if not "ticket" in filters: return ticket_realm = Resource("ticket") with self.env.db_query as db: sql, args = search_to_sql( db, ["summary", "keywords", "description", "reporter", "cc", db.cast("id", "text")], terms ) sql2, args2 = search_to_sql(db, ["newvalue"], terms) sql3, args3 = search_to_sql(db, ["value"], terms) ticketsystem = TicketSystem(self.env) if req.args.get("product"): productsql = "product='%s' AND" % req.args.get("product") else: productsql = "" for summary, desc, author, type, tid, ts, status, resolution in db( """SELECT summary, description, reporter, type, id, time, status, resolution FROM ticket WHERE (%s id IN ( SELECT id FROM ticket WHERE %s UNION SELECT ticket FROM ticket_change WHERE field='comment' AND %s UNION SELECT ticket FROM ticket_custom WHERE %s )) """ % (productsql, sql, sql2, sql3), args + args2 + args3, ): t = ticket_realm(id=tid) if "TICKET_VIEW" in req.perm(t): yield ( req.href.ticket(tid), tag_( "%(title)s: %(message)s", title=tag.span(get_resource_shortname(self.env, t), class_=status), message=ticketsystem.format_summary(summary, status, resolution, type), ), from_utimestamp(ts), author, shorten_result(desc, terms), ) # Attachments for result in AttachmentModule(self.env).get_search_results(req, ticket_realm, terms): yield result
def _get_ticket_fields(self, data): """ Return a list of the ticket fields corresponding to the output columns The data returned is used for ticket field input """ ts = TicketSystem(self.env) fields = ts.get_ticket_fields() results = [] for header in data['headers'][1:]: for field in fields: if field['name'] == header['name']: results.append(field) continue continue return results
def setUp(self): tmpdir = os.path.realpath(tempfile.gettempdir()) self.env = EnvironmentStub(enable=['trac.*', AuthzPolicy], path=tmpdir) self.env.config.set('trac', 'permission_policies', 'AuthzPolicy, DefaultPermissionPolicy') self.env.config.set('ticket', 'restrict_owner', True) self.perm_sys = PermissionSystem(self.env) self.env.insert_known_users([ ('user1', '', ''), ('user2', '', ''), ('user3', '', ''), ('user4', '', '') ]) self.perm_sys.grant_permission('user1', 'TICKET_MODIFY') self.perm_sys.grant_permission('user2', 'TICKET_VIEW') self.perm_sys.grant_permission('user3', 'TICKET_MODIFY') self.perm_sys.grant_permission('user4', 'TICKET_MODIFY') self.authz_file = os.path.join(tmpdir, 'trac-authz-policy') create_file(self.authz_file) self.env.config.set('authz_policy', 'authz_file', self.authz_file) self.ctlr = TicketSystem(self.env).action_controllers[0] self.req1 = Mock(authname='user1', args={}, perm=PermissionCache(self.env, 'user1')) self.ticket = Ticket(self.env) self.ticket['status'] = 'new' self.ticket.insert()
def setUp(self): self.env = EnvironmentStub(default_data=True) self.perm_sys = PermissionSystem(self.env) self.ctlr = TicketSystem(self.env).action_controllers[0] self.ticket = Ticket(self.env) self.ticket['status'] = 'new' self.ticket.insert() with self.env.db_transaction as db: for user in ('user1', 'user2', 'user3', 'user4'): db("INSERT INTO session VALUES (%s, %s, %s)", (user, 1, 0)) permissions = [ ('user1', 'TICKET_EDIT_CC'), ('user2', 'TICKET_EDIT_CC'), ('user2', 'TICKET_BATCH_MODIFY'), ('user3', 'TICKET_ADMIN'), ('user4', 'TICKET_VIEW'), ('user1', 'group1'), ('user2', 'group1'), ('user2', 'group2'), ('user3', 'group2'), ('user4', 'group3') ] for perm in permissions: self.perm_sys.grant_permission(*perm) self.req = Mock(authname='user1', args={}, perm=PermissionCache(self.env, 'user0')) self.expected = """\
def setUp(self): self.env = EnvironmentStub(default_data=True) config = self.env.config config.set('ticket-workflow', 'change_owner', 'new -> new') config.set('ticket-workflow', 'change_owner.operations', 'set_owner') self.ctlr = TicketSystem(self.env).action_controllers[0] self.ticket_module = TicketModule(self.env)
def _implementation(db): tkt = Ticket(self.env, ticket_id) ts = TicketSystem(self.env) tm = TicketModule(self.env) if action not in ts.get_available_actions(req, tkt): raise ValueError(["This ticket cannot be moved to this status,\ perhaps the ticket has been updated by someone else."]) field_changes, problems = \ tm.get_ticket_changes(req, tkt, action) if problems: raise ValueError(problems) tm._apply_ticket_changes(tkt, field_changes) valid = tm._validate_ticket(req, tkt, force_collision_check=True) if not valid: raise ValueError(req.chrome['warnings']) else: tkt.save_changes(req.authname, "", when=datetime.now(utc))
def _index_ticket(self, ticket): ticketsystem = TicketSystem(self.env) resource_name = get_resource_shortname(self.env, ticket.resource) resource_desc = ticketsystem.get_resource_description(ticket.resource, format="summary") so = FullTextSearchObject( self.project, ticket.resource, title=u"%(title)s: %(message)s" % {"title": resource_name, "message": resource_desc}, author=ticket.values.get("reporter"), changed=ticket.values.get("changetime"), created=ticket.values.get("time"), tags=ticket.values.get("keywords"), involved=re.split(r"[;,\s]+", ticket.values.get("cc", "")) or ticket.values.get("reporter"), popularity=0, # FIXME oneline=shorten_result(ticket.values.get("description", "")), body=u"%r" % (ticket.values,), comments=[t[4] for t in ticket.get_changelog()], ) self.backend.create(so, quiet=True) self.log.debug("Ticket added for indexing: %s", ticket)
def setUp(self): self.env = EnvironmentStub(default_data=True) self.perm_sys = PermissionSystem(self.env) self.ctlr = TicketSystem(self.env).action_controllers[0] self.req1 = Mock(authname='user1', args={}, perm=PermissionCache(self.env, 'user1')) self.req2 = Mock(authname='user2', args={}, perm=PermissionCache(self.env, 'user2')) self.ticket = Ticket(self.env) self.ticket['status'] = 'invalid' self.ticket.insert()
def getActions(self, req, id): """Returns the actions that can be performed on the ticket as a list of `[action, label, hints, [input_fields]]` elements, where `input_fields` is a list of `[name, value, [options]]` for any required action inputs.""" ts = TicketSystem(self.env) t = model.Ticket(self.env, id) actions = [] for action in ts.get_available_actions(req, t): fragment = genshi.builder.Fragment() hints = [] first_label = None for controller in ts.action_controllers: if action in [c_action for c_weight, c_action \ in controller.get_ticket_actions(req, t)]: label, widget, hint = \ controller.render_ticket_action_control(req, t, action) fragment += widget hints.append(to_unicode(hint).rstrip('.') + '.') first_label = first_label == None and label or first_label controls = [] for elem in fragment.children: if not isinstance(elem, genshi.builder.Element): continue if elem.tag == 'input': controls.append((elem.attrib.get('name'), elem.attrib.get('value'), [])) elif elem.tag == 'select': value = '' options = [] for opt in elem.children: if not (opt.tag == 'option' and opt.children): continue option = opt.children[0] options.append(option) if opt.attrib.get('selected'): value = option controls.append((elem.attrib.get('name'), value, options)) actions.append((action, first_label, " ".join(hints), controls)) return actions
def ticket_created(self, ticket): ticketsystem = TicketSystem(self.env) resource_name = get_resource_shortname(self.env, ticket.resource) resource_desc = ticketsystem.get_resource_description(ticket.resource, format='summary') so = FullTextSearchObject( self.project, ticket.resource, title = u"%(title)s: %(message)s" % {'title': resource_name, 'message': resource_desc}, author = ticket.values.get('reporter'), changed = ticket.values.get('changetime'), created = ticket.values.get('time'), tags = ticket.values.get('keywords'), involved = re.split(r'[;,\s]+', ticket.values.get('cc', '')) or ticket.values.get('reporter'), popularity = 0, #FIXME oneline = shorten_result(ticket.values.get('description', '')), body = u'%r' % (ticket.values,), comments = [t[4] for t in ticket.get_changelog()], ) self.backend.create(so, quiet=True) self._update_ticket(ticket) self.log.debug("Ticket added for indexing: %s", ticket)
def __init__(self, env, tkt_id=None, version=None): self.env = env self.fields = TicketSystem(self.env).get_ticket_fields() self.editable_fields = \ set(f['name'] for f in self.fields if f['name'] not in self.protected_fields) self.std_fields, self.custom_fields, self.time_fields = [], [], [] for f in self.fields: if f.get('custom'): self.custom_fields.append(f['name']) else: self.std_fields.append(f['name']) if f['type'] == 'time': self.time_fields.append(f['name']) self.values = {} if tkt_id is not None: tkt_id = int(tkt_id) self._fetch_ticket(tkt_id) else: self._init_defaults() self.id = None self.version = version self._old = {}
def getAvailableActions(self, req, id): """Returns the actions that can be performed on the ticket.""" ticketSystem = TicketSystem(self.env) t = model.Ticket(self.env, id) return ticketSystem.get_available_actions(t, req.perm)
class ConfigurableTicketWorkflowTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True) config = self.env.config config.set('ticket-workflow', 'change_owner', 'new -> new') config.set('ticket-workflow', 'change_owner.operations', 'set_owner') self.ctlr = TicketSystem(self.env).action_controllers[0] self.ticket_module = TicketModule(self.env) def tearDown(self): self.env.reset_db() def _add_component(self, name='test', owner='owner1'): component = Component(self.env) component.name = name component.owner = owner component.insert() def _reload_workflow(self): self.ctlr.actions = self.ctlr.get_all_actions() def test_get_all_actions_custom_attribute(self): """Custom attribute in ticket-workflow.""" config = self.env.config['ticket-workflow'] config.set('resolve.set_milestone', 'reject') all_actions = self.ctlr.get_all_actions() resolve_action = None for name, attrs in all_actions.items(): if name == 'resolve': resolve_action = attrs self.assertIsNotNone(resolve_action) self.assertIn('set_milestone', resolve_action.keys()) self.assertEqual('reject', resolve_action['set_milestone']) 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 = MockRequest(self.env, 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 test_component_change(self): """New ticket owner is updated when the component is changed. """ self._add_component('component3', 'cowner3') self._add_component('component4', 'cowner4') ticket = Ticket(self.env) ticket.populate({ 'reporter': 'reporter1', 'summary': 'the summary', 'component': 'component3', 'owner': 'cowner3', 'status': 'new', }) tkt_id = ticket.insert() req = MockRequest(self.env, method='POST', args={ 'id': tkt_id, 'field_component': 'component4', 'submit': True, 'action': 'leave', 'view_time': str(to_utimestamp(ticket['changetime'])), }) self.assertRaises(RequestDone, self.ticket_module.process_request, req) ticket = Ticket(self.env, tkt_id) self.assertEqual('component4', ticket['component']) self.assertEqual('cowner4', ticket['owner']) def test_component_change_and_owner_change(self): """New ticket owner is not updated if owner is explicitly changed. """ self._add_component('component3', 'cowner3') self._add_component('component4', 'cowner4') ticket = Ticket(self.env) ticket.populate({ 'reporter': 'reporter1', 'summary': 'the summary', 'component': 'component3', 'status': 'new', }) tkt_id = ticket.insert() req = MockRequest(self.env, method='POST', args={ 'id': tkt_id, 'field_component': 'component4', 'submit': True, 'action': 'change_owner', 'action_change_owner_reassign_owner': 'owner1', 'view_time': str(to_utimestamp(ticket['changetime'])), }) self.assertRaises(RequestDone, self.ticket_module.process_request, req) ticket = Ticket(self.env, tkt_id) self.assertEqual('component4', ticket['component']) self.assertEqual('owner1', ticket['owner']) def test_old_owner_not_old_component_owner(self): """New ticket owner is not updated if old owner is not the owner of the old component. """ self._add_component('component3', 'cowner3') self._add_component('component4', 'cowner4') ticket = Ticket(self.env) ticket.populate({ 'reporter': 'reporter1', 'summary': 'the summary', 'component': 'component3', 'owner': 'owner1', 'status': 'new', }) tkt_id = ticket.insert() req = MockRequest(self.env, method='POST', args={ 'id': tkt_id, 'field_component': 'component4', 'submit': True, 'action': 'leave', 'view_time': str(to_utimestamp(ticket['changetime'])), }) self.assertRaises(RequestDone, self.ticket_module.process_request, req) ticket = Ticket(self.env, tkt_id) self.assertEqual('component4', ticket['component']) self.assertEqual('owner1', ticket['owner']) def test_new_component_has_no_owner(self): """Ticket is not disowned when the component is changed to a component with no owner. """ self._add_component('component3', 'cowner3') self._add_component('component4', '') ticket = Ticket(self.env) ticket.populate({ 'reporter': 'reporter1', 'summary': 'the summary', 'component': 'component3', 'owner': 'cowner3', 'status': 'new', }) tkt_id = ticket.insert() req = MockRequest(self.env, method='POST', args={ 'id': tkt_id, 'field_component': 'component4', 'submit': True, 'action': 'leave', 'view_time': str(to_utimestamp(ticket['changetime'])), }) self.assertRaises(RequestDone, self.ticket_module.process_request, req) ticket = Ticket(self.env, tkt_id) self.assertEqual('component4', ticket['component']) self.assertEqual('cowner3', ticket['owner']) def test_transition_to_star(self): """Workflow hint is not be added in a workflow transition to *, for example: <none> -> * 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') label, control, hints = \ self.ctlr.render_ticket_action_control(req, ticket, 'create_and_triage') self.assertEqual('create and triage', label) self.assertEqual('', unicode(control)) self.assertEqual('', unicode(hints))
def _get_actions(self, ticket_dict): ts = TicketSystem(self.env) ticket = Ticket(self.env) ticket.populate(ticket_dict) id = ticket.insert() return ts.get_available_actions(self.req, Ticket(self.env, id))
def expand_macro(self, formatter, name, content, realms=[]): """Evaluate macro call and render results. Calls from web-UI come with pre-processed realm selection. """ env = self.env req = formatter.req tag_system = TagSystem(env) all_realms = tag_system.get_taggable_realms() if not all_realms: # Tag providers are required, no result without at least one. return '' args, kw = parse_args(content) query = args and args[0].strip() or None if not realms: # Check macro arguments for realms (typical wiki macro call). realms = 'realm' in kw and kw['realm'].split('|') or [] if query: # Add realms from query expression. realms.extend(query_realms(query, all_realms)) # Remove redundant realm selection for performance. if set(realms) == all_realms: query = re.sub('(^|\W)realm:\S+(\W|$)', ' ', query).strip() if name == 'TagCloud': # Set implicit 'all tagged realms' as default. if not realms: realms = all_realms if query: all_tags = Counter() # Require per resource query including view permission checks. for resource, tags in tag_system.query(req, query): all_tags.update(tags) else: # Allow faster per tag query, side steps permission checks. all_tags = tag_system.get_all_tags(req, realms=realms) mincount = 'mincount' in kw and kw['mincount'] or None return self.render_cloud(req, all_tags, caseless_sort=self.caseless_sort, mincount=mincount, realms=realms) elif name == 'ListTagged': if content and _OBSOLETE_ARGS_RE.search(content): data = {'warning': 'obsolete_args'} else: data = {'warning': None} context = formatter.context # Use TagsQuery arguments (most likely wiki macro calls). cols = 'cols' in kw and kw['cols'] or self.default_cols format = 'format' in kw and kw['format'] or self.default_format if not realms: # Apply ListTagged defaults to macro call w/o realm. realms = list(set(all_realms)-set(self.exclude_realms)) if not realms: return '' query = '(%s) (%s)' % (query or '', ' or '.join(['realm:%s' % (r) for r in realms])) query_result = tag_system.query(req, query) excludes = [exc.strip() for exc in kw.get('exclude', '' ).split(':') if exc.strip()] if excludes and query_result: filtered_result = [(resource, tags) for resource, tags in query_result if not any(fnmatchcase(resource.id, exc) for exc in excludes)] query_result = filtered_result if not query_result: return '' 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') if format == 'table': cols = [col for col in cols.split('|') if col in self.supported_cols] # Use available translations from Trac core. try: labels = TicketSystem(env).get_ticket_field_labels() labels['id'] = _('Id') except AttributeError: # Trac 0.11 neither has the attribute nor uses i18n. labels = {'id': 'Id', 'description': 'Description'} labels['realm'] = _('Realm') labels['tags'] = _('Tags') headers = [{'label': labels.get(col)} for col in cols] data.update({'cols': cols, 'headers': headers}) results = sorted(query_result, key=lambda r: \ embedded_numbers(to_unicode(r[0].id))) results = self._paginate(req, results, realms) rows = [] for resource, tags in results: desc = tag_system.describe_tagged_resource(req, resource) tags = sorted(tags) wiki_desc = format_to_oneliner(env, context, desc) if tags: rendered_tags = [_link(Resource('tag', tag)) for tag in tags] if 'oldlist' == format: resource_link = _link(resource) else: resource_link = builder.a(wiki_desc, href=get_resource_url( env, resource, context.href)) if 'table' == format: cells = [] for col in cols: if col == 'id': cells.append(_link(resource)) # Don't duplicate links to resource in both. elif col == 'description' and 'id' in cols: cells.append(wiki_desc) elif col == 'description': cells.append(resource_link) elif col == 'realm': cells.append(resource.realm) elif col == 'tags': cells.append( builder([(tag, ' ') for tag in rendered_tags])) rows.append({'cells': cells}) continue rows.append({'desc': wiki_desc, 'rendered_tags': None, 'resource_link': _link(resource)}) data.update({'format': format, 'paginator': results, 'results': rows, 'tags_url': req.href('tags')}) # Work around a bug in trac/templates/layout.html, that causes a # TypeError for the wiki macro call, if we use add_link() alone. add_stylesheet(req, 'common/css/search.css') return Chrome(env).render_template( req, 'listtagged_results.html', data, 'text/html', True)
def expand_macro(self, formatter, name, text, args): template_data = {'css_class': 'trac-kanban-board'} template_file = 'kanbanboard.html' board = None template_data['height'] = '300px' if args: template_data['height'] = args.get('height', '300px') project_name = self.env.path.split('/')[-1] page_name = formatter.req.path_info.split('/')[-1] is_editable = 'WIKI_MODIFY' in formatter.req.perm and 'TICKET_MODIFY' in formatter.req.perm js_globals = { 'KANBAN_BOARD_ID': page_name, 'TRAC_PROJECT_NAME': project_name, 'TRAC_USER_NAME': formatter.req.authname, 'IS_EDITABLE': is_editable } if not self.ticket_fields: self.ticket_fields = TicketSystem(self.env).get_ticket_fields() if text is None: template_data['error'] = 'Board data is not defined' template_data['usage'] = format_to_html(self.env, formatter.context, self.__doc__) else: try: board = KanbanBoard(page_name, [], self.ticket_fields, self.env, self.log) except InvalidDataError as e: template_data['error'] = e.msg template_data['usage'] = format_to_html( self.env, formatter.context, self.__doc__) except InvalidFieldError as e: template_data[ 'error'] = 'Invalid ticket fields: %s' % ', '.join( e.fields) valid_fields = map(lambda x: x['name'], self.ticket_fields) template_data[ 'usage'] = 'Valid field names are: %s.' % ', '.join( valid_fields) if board: # TICKET_FIELDS is comma-separated list of user defined ticket field names js_globals['TICKET_FIELDS'] = board.get_field_string() add_stylesheet(formatter.req, 'trackanbanboard/css/kanbanboard.css') add_script_data(formatter.req, js_globals) if 'error' in template_data: template_file = 'kanbanerror.html' else: add_script(formatter.req, 'trackanbanboard/js/libs/jquery-1.8.3.js') add_script( formatter.req, 'trackanbanboard/js/libs/jquery-ui-1.9.2.custom.min.js') add_script(formatter.req, 'trackanbanboard/js/libs/knockout-2.2.0.js') add_script(formatter.req, 'trackanbanboard/js/libs/knockout.mapping.js') add_script(formatter.req, 'trackanbanboard/js/libs/knockout-sortable.min.js') add_script(formatter.req, 'trackanbanboard/js/kanbanutil.js') add_script(formatter.req, 'trackanbanboard/js/kanbanboard.js') add_stylesheet( formatter.req, 'trackanbanboard/css/jquery-ui-1.9.2.custom.min.css') return Chrome(self.env).render_template( formatter.req, template_file, template_data, None, fragment=True).render(strip_whitespace=False)
def test_custom_field_select_without_options(self): self.env.config.set('ticket-custom', 'test', 'select') self.env.config.set('ticket-custom', 'test.label', 'Test') self.env.config.set('ticket-custom', 'test.value', '1') fields = TicketSystem(self.env).get_custom_fields() self.assertEqual(0, len(fields))
def get_user_list(self): return TicketSystem(self.env).get_allowed_owners()
class ResetActionTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True) self.perm_sys = PermissionSystem(self.env) self.ctlr = TicketSystem(self.env).action_controllers[0] self.req1 = MockRequest(self.env, authname='user1') self.req2 = MockRequest(self.env, authname='user2') self.ticket = insert_ticket(self.env, status='invalid') def tearDown(self): self.env.reset_db() def _reload_workflow(self): self.ctlr.actions = self.ctlr.get_all_actions() def test_default_reset_action(self): """Default reset action.""" self.perm_sys.grant_permission('user2', 'TICKET_ADMIN') self._reload_workflow() actions1 = self.ctlr.get_ticket_actions(self.req1, self.ticket) actions2 = self.ctlr.get_ticket_actions(self.req2, self.ticket) chgs2 = self.ctlr.get_ticket_changes(self.req2, self.ticket, '_reset') self.assertEqual(1, len(actions1)) self.assertNotIn((0, '_reset'), actions1) self.assertEqual(2, len(actions2)) self.assertIn((0, '_reset'), actions2) self.assertEqual('new', chgs2['status']) def test_default_reset_action_without_new_state(self): """Default reset action not available when no new state.""" self.perm_sys.grant_permission('user2', 'TICKET_ADMIN') config = self.env.config # Replace 'new' state with 'untriaged' config.set('ticket-workflow', 'create', '<none> -> untriaged') config.set('ticket-workflow', 'accept', 'untriaged,assigned,accepted,reopened -> accepted') config.set('ticket-workflow', 'resolve', 'untriaged,assigned,accepted,reopened -> closed') config.set('ticket-workflow', 'reassign', 'untriaged,assigned,accepted,reopened -> assigned') self._reload_workflow() actions = self.ctlr.get_ticket_actions(self.req2, self.ticket) self.assertEqual(1, len(actions)) self.assertNotIn((0, '_reset'), actions) def test_custom_reset_action(self): """Custom reset action in [ticket-workflow] section.""" config = self.env.config['ticket-workflow'] config.set('_reset', '-> review') config.set('_reset.operations', 'reset_workflow') config.set('_reset.permissions', 'TICKET_BATCH_MODIFY') config.set('_reset.default', 2) self.perm_sys.grant_permission('user2', 'TICKET_BATCH_MODIFY') self._reload_workflow() actions1 = self.ctlr.get_ticket_actions(self.req1, self.ticket) actions2 = self.ctlr.get_ticket_actions(self.req2, self.ticket) chgs2 = self.ctlr.get_ticket_changes(self.req2, self.ticket, '_reset') self.assertEqual(1, len(actions1)) self.assertNotIn((2, '_reset'), actions1) self.assertEqual(2, len(actions2)) self.assertIn((2, '_reset'), actions2) self.assertEqual('review', chgs2['status'])
class ConfigurableTicketWorkflowTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub() config = self.env.config config.set('ticket-workflow', 'change_owner', 'new -> new') config.set('ticket-workflow', 'change_owner.operations', 'set_owner') self.ctlr = TicketSystem(self.env).action_controllers[0] self.ticket_module = TicketModule(self.env) def tearDown(self): self.env.reset_db() def _add_component(self, name='test', owner='owner1'): component = Component(self.env) component.name = name component.owner = owner component.insert() def _reload_workflow(self): self.ctlr.actions = self.ctlr.get_all_actions() def test_get_all_actions_custom_attribute(self): """Custom attribute in ticket-workflow.""" config = self.env.config['ticket-workflow'] config.set('resolve.set_milestone', 'reject') all_actions = self.ctlr.get_all_actions() resolve_action = None for name, attrs in all_actions.items(): if name == 'resolve': resolve_action = attrs self.assertIsNotNone(resolve_action) self.assertIn('set_milestone', list(resolve_action)) self.assertEqual('reject', resolve_action['set_milestone']) 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 = MockRequest(self.env, 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 test_component_change(self): """New ticket owner is updated when the component is changed. """ self._add_component('component3', 'cowner3') self._add_component('component4', 'cowner4') ticket = insert_ticket(self.env, reporter='reporter1', summary='the summary', component='component3', owner='cowner3', status='new') req = MockRequest(self.env, method='POST', args={ 'id': ticket.id, 'field_component': 'component4', 'submit': True, 'action': 'leave', 'view_time': str(to_utimestamp(ticket['changetime'])), }) self.assertRaises(RequestDone, self.ticket_module.process_request, req) ticket = Ticket(self.env, ticket.id) self.assertEqual('component4', ticket['component']) self.assertEqual('cowner4', ticket['owner']) def test_component_change_and_owner_change(self): """New ticket owner is not updated if owner is explicitly changed. """ self._add_component('component3', 'cowner3') self._add_component('component4', 'cowner4') ticket = insert_ticket(self.env, reporter='reporter1', summary='the summary', component='component3', status='new') req = MockRequest(self.env, method='POST', args={ 'id': ticket.id, 'field_component': 'component4', 'submit': True, 'action': 'change_owner', 'action_change_owner_reassign_owner': 'owner1', 'view_time': str(to_utimestamp(ticket['changetime'])), }) self.assertRaises(RequestDone, self.ticket_module.process_request, req) ticket = Ticket(self.env, ticket.id) self.assertEqual('component4', ticket['component']) self.assertEqual('owner1', ticket['owner']) def test_old_owner_not_old_component_owner(self): """New ticket owner is not updated if old owner is not the owner of the old component. """ self._add_component('component3', 'cowner3') self._add_component('component4', 'cowner4') ticket = insert_ticket(self.env, reporter='reporter1', summary='the summary', component='component3', owner='owner1', status='new') req = MockRequest(self.env, method='POST', args={ 'id': ticket.id, 'field_component': 'component4', 'submit': True, 'action': 'leave', 'view_time': str(to_utimestamp(ticket['changetime'])), }) self.assertRaises(RequestDone, self.ticket_module.process_request, req) ticket = Ticket(self.env, ticket.id) self.assertEqual('component4', ticket['component']) self.assertEqual('owner1', ticket['owner']) def test_new_component_has_no_owner(self): """Ticket is not disowned when the component is changed to a component with no owner. """ self._add_component('component3', 'cowner3') self._add_component('component4', '') ticket = insert_ticket(self.env, reporter='reporter1', summary='the summary', component='component3', owner='cowner3', status='new') req = MockRequest(self.env, method='POST', args={ 'id': ticket.id, 'field_component': 'component4', 'submit': True, 'action': 'leave', 'view_time': str(to_utimestamp(ticket['changetime'])), }) self.assertRaises(RequestDone, self.ticket_module.process_request, req) ticket = Ticket(self.env, ticket.id) self.assertEqual('component4', ticket['component']) self.assertEqual('cowner3', ticket['owner']) def _test_get_allowed_owners(self): ticket = insert_ticket(self.env, summary='Ticket 1') self.env.insert_users([('user1', None, None, 1), ('user2', None, None, 1), ('user3', None, None, 1)]) ps = PermissionSystem(self.env) for user in ('user1', 'user3'): ps.grant_permission(user, 'TICKET_MODIFY') self.env.config.set('ticket', 'restrict_owner', True) return ticket def test_get_allowed_owners_returns_set_owner_list(self): """Users specified in `set_owner` for the action are returned.""" req = None action = {'set_owner': ['user4', 'user5']} ticket = self._test_get_allowed_owners() self.assertEqual(['user4', 'user5'], self.ctlr.get_allowed_owners(req, ticket, action)) def test_get_allowed_owners_returns_user_with_ticket_modify(self): """Users with TICKET_MODIFY are are returned if `set_owner` is not specified for the action. """ req = None action = {} ticket = self._test_get_allowed_owners() self.assertEqual(['user1', 'user3'], self.ctlr.get_allowed_owners(req, ticket, action)) def test_transition_to_star(self): """Workflow hint is not be added in a workflow transition to *, for example: <none> -> * 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') label, control, hints = \ self.ctlr.render_ticket_action_control(req, ticket, 'create_and_triage') self.assertEqual('create and triage', label) self.assertEqual('', unicode(control)) self.assertEqual('', unicode(hints)) def test_get_actions_by_operation_for_req(self): """Request with no permission checking.""" req = MockRequest(self.env, path_info='/ticket/1') ticket = insert_ticket(self.env, status='new') actions = self.ctlr.get_actions_by_operation_for_req( req, ticket, 'set_owner') self.assertEqual([(0, u'change_owner'), (0, u'reassign')], actions) def test_get_actions_by_operation_for_req_with_ticket_modify(self): """User without TICKET_MODIFY won't have reassign action.""" req = MockRequest(self.env, authname='user1', path_info='/ticket/1') ticket = insert_ticket(self.env, status='new') actions = self.ctlr.get_actions_by_operation_for_req( req, ticket, 'set_owner') self.assertEqual([(0, u'change_owner')], actions) def test_get_actions_by_operation_for_req_without_ticket_modify(self): """User with TICKET_MODIFY will have reassign action.""" PermissionSystem(self.env).grant_permission('user1', 'TICKET_MODIFY') req = MockRequest(self.env, authname='user1', path_info='/ticket/1') ticket = insert_ticket(self.env, status='new') actions = self.ctlr.get_actions_by_operation_for_req( req, ticket, 'set_owner') self.assertEqual([(0, u'change_owner'), (0, u'reassign')], actions) def test_ignores_other_operations(self): """Ignores operations not defined by ConfigurableTicketWorkflow. """ self.env.config.set('ticket-workflow', 'review', 'assigned -> review') self.env.config.set('ticket-workflow', 'review.operations', 'CodeReview') ctw = ConfigurableTicketWorkflow(self.env) ticket = Ticket(self.env) ticket.populate({'summary': '#13013', 'status': 'assigned'}) ticket.insert() req = MockRequest(self.env) self.assertNotIn((0, 'review'), ctw.get_ticket_actions(req, ticket))
def _render_view(self, req, id): """Retrieve the report results and pre-process them for rendering.""" r = Report(self.env, id) title, description, sql = r.title, r.description, r.query try: args = self.get_var_args(req) sql = self.get_default_var_args(args, sql) except ValueError as e: raise TracError(_("Report failed: %(error)s", error=e)) # If this is a saved custom query, redirect to the query module # # A saved query is either an URL query (?... or query:?...), # or a query language expression (query:...). # # It may eventually contain newlines, for increased clarity. # query = ''.join([line.strip() for line in sql.splitlines()]) if query and (query[0] == '?' or query.startswith('query:?')): query = query if query[0] == '?' else query[6:] report_id = 'report=%s' % id if 'report=' in query: if report_id not in query: err = _( 'When specified, the report number should be ' '"%(num)s".', num=id) req.redirect(req.href.report(id, action='edit', error=err)) else: if query[-1] != '?': query += '&' query += report_id req.redirect(req.href.query() + quote_query_string(query)) elif query.startswith('query:'): from trac.ticket.query import Query, QuerySyntaxError try: query = Query.from_string(self.env, query[6:], report=id) except QuerySyntaxError as e: req.redirect( req.href.report(id, action='edit', error=to_unicode(e))) else: req.redirect(query.get_href(req.href)) format = req.args.get('format') if format == 'sql': self._send_sql(req, id, title, description, sql) title = '{%i} %s' % (id, title) report_resource = Resource(self.realm, id) req.perm(report_resource).require('REPORT_VIEW') context = web_context(req, report_resource) page = req.args.getint('page', 1) default_max = { 'rss': self.items_per_page_rss, 'csv': 0, 'tab': 0 }.get(format, self.items_per_page) max = req.args.get('max') limit = as_int(max, default_max, min=0) # explict max takes precedence offset = (page - 1) * limit sort_col = req.args.get('sort', '') asc = req.args.getbool('asc', True) def report_href(**kwargs): """Generate links to this report preserving user variables, and sorting and paging variables. """ params = args.copy() if sort_col: params['sort'] = sort_col params['page'] = page if max: params['max'] = max params.update(kwargs) params['asc'] = as_int(params.get('asc'), asc, min=0, max=1) return req.href.report(id, params) data = { 'action': 'view', 'report': { 'id': id, 'resource': report_resource }, 'context': context, 'title': sub_vars(title, args), 'description': sub_vars(description or '', args), 'max': limit, 'args': args, 'show_args_form': False, 'message': None, 'paginator': None, 'report_href': report_href, } res = self.execute_paginated_report(req, id, sql, args, limit, offset) if len(res) == 2: e, sql = res data['message'] = \ tag_("Report execution failed: %(error)s %(sql)s", error=tag.pre(exception_to_unicode(e)), sql=tag(tag.hr(), tag.pre(sql, style="white-space: pre"))) return 'report_view.html', data, None cols, results, num_items, missing_args, limit_offset = res need_paginator = limit > 0 and limit_offset need_reorder = limit_offset is None results = [list(row) for row in results] numrows = len(results) paginator = None if need_paginator: paginator = Paginator(results, page - 1, limit, num_items) data['paginator'] = paginator if paginator.has_next_page: add_link(req, 'next', report_href(page=page + 1), _('Next Page')) if paginator.has_previous_page: add_link(req, 'prev', report_href(page=page - 1), _('Previous Page')) pagedata = [] shown_pages = paginator.get_shown_pages(21) for p in shown_pages: pagedata.append([ report_href(page=p), None, str(p), _('Page %(num)d', num=p) ]) fields = ['href', 'class', 'string', 'title'] paginator.shown_pages = [dict(zip(fields, p)) for p in pagedata] paginator.current_page = { 'href': None, 'class': 'current', 'string': str(paginator.page + 1), 'title': None } numrows = paginator.num_items # Place retrieved columns in groups, according to naming conventions # * _col_ means fullrow, i.e. a group with one header # * col_ means finish the current group and start a new one field_labels = TicketSystem(self.env).get_ticket_field_labels() header_groups = [[]] for idx, col in enumerate(cols): if col in field_labels: title = field_labels[col] else: title = col.strip('_').capitalize() header = { 'col': col, 'title': title, 'hidden': False, 'asc': None, } if col == sort_col: header['asc'] = asc if not paginator and need_reorder: # this dict will have enum values for sorting # and will be used in sortkey(), if non-empty: sort_values = {} if sort_col in ('status', 'resolution', 'priority', 'severity'): # must fetch sort values for that columns # instead of comparing them as strings with self.env.db_query as db: for name, value in db( "SELECT name, %s FROM enum WHERE type=%%s" % db.cast('value', 'int'), (sort_col, )): sort_values[name] = value def sortkey(row): val = row[idx] # check if we have sort_values, then use them as keys. if sort_values: return sort_values.get(val) # otherwise, continue with string comparison: if isinstance(val, basestring): val = val.lower() return val results = sorted(results, key=sortkey, reverse=(not asc)) header_group = header_groups[-1] if col.startswith('__') and col.endswith('__'): # __col__ header['hidden'] = True elif col[0] == '_' and col[-1] == '_': # _col_ header_group = [] header_groups.append(header_group) header_groups.append([]) elif col[0] == '_': # _col header['hidden'] = True elif col[-1] == '_': # col_ header_groups.append([]) header_group.append(header) # Structure the rows and cells: # - group rows according to __group__ value, if defined # - group cells the same way headers are grouped chrome = Chrome(self.env) row_groups = [] authorized_results = [] prev_group_value = None for row_idx, result in enumerate(results): col_idx = 0 cell_groups = [] row = {'cell_groups': cell_groups} realm = TicketSystem.realm parent_realm = '' parent_id = '' email_cells = [] for header_group in header_groups: cell_group = [] for header in header_group: value = cell_value(result[col_idx]) cell = {'value': value, 'header': header, 'index': col_idx} col = header['col'] col_idx += 1 # Detect and create new group if col == '__group__' and value != prev_group_value: prev_group_value = value # Brute force handling of email in group by header row_groups.append( (value and chrome.format_author(req, value), [])) # Other row properties row['__idx__'] = row_idx if col in self._html_cols: row[col] = value if col in ('report', 'ticket', 'id', '_id'): row['id'] = value # Special casing based on column name col = col.strip('_') if col in ('reporter', 'cc', 'owner'): email_cells.append(cell) elif col == 'realm': realm = value elif col == 'parent_realm': parent_realm = value elif col == 'parent_id': parent_id = value cell_group.append(cell) cell_groups.append(cell_group) if parent_realm: resource = Resource(realm, row.get('id'), parent=Resource(parent_realm, parent_id)) else: resource = Resource(realm, row.get('id')) # FIXME: for now, we still need to hardcode the realm in the action if resource.realm.upper() + '_VIEW' not in req.perm(resource): continue authorized_results.append(result) if email_cells: for cell in email_cells: emails = chrome.format_emails(context.child(resource), cell['value']) result[cell['index']] = cell['value'] = emails row['resource'] = resource if row_groups: row_group = row_groups[-1][1] else: row_group = [] row_groups = [(None, row_group)] row_group.append(row) data.update({ 'header_groups': header_groups, 'row_groups': row_groups, 'numrows': numrows }) if format == 'rss': data['context'] = web_context(req, report_resource, absurls=True) return 'report.rss', data, 'application/rss+xml' elif format == 'csv': filename = 'report_%s.csv' % id if id else 'report.csv' self._send_csv(req, cols, authorized_results, mimetype='text/csv', filename=filename) elif format == 'tab': filename = 'report_%s.tsv' % id if id else 'report.tsv' self._send_csv(req, cols, authorized_results, '\t', mimetype='text/tab-separated-values', filename=filename) else: p = page if max is not None else None add_link(req, 'alternate', auth_link(req, report_href(format='rss', page=None)), _('RSS Feed'), 'application/rss+xml', 'rss') add_link(req, 'alternate', report_href(format='csv', page=p), _('Comma-delimited Text'), 'text/plain') add_link(req, 'alternate', report_href(format='tab', page=p), _('Tab-delimited Text'), 'text/plain') if 'REPORT_SQL_VIEW' in req.perm(self.realm, id): add_link(req, 'alternate', req.href.report(id=id, format='sql'), _('SQL Query'), 'text/plain') # reuse the session vars of the query module so that # the query navigation links on the ticket can be used to # navigate report results as well try: req.session['query_tickets'] = \ ' '.join([str(int(row['id'])) for rg in row_groups for row in rg[1]]) req.session['query_href'] = \ req.session['query_href'] = report_href() # Kludge: we have to clear the other query session # variables, but only if the above succeeded for var in ('query_constraints', 'query_time'): if var in req.session: del req.session[var] except (ValueError, KeyError): pass if set(data['args']) - set(['USER']): data['show_args_form'] = True add_script(req, 'common/js/folding.js') if missing_args: add_warning( req, _('The following arguments are missing: %(args)s', args=", ".join(missing_args))) return 'report_view.html', data, None
def render_ticket_action_control(self, req, ticket, action): self.log.debug('render_ticket_action_control: action "%s"' % action) this_action = self.actions[action] status = this_action['newstate'] operations = this_action['operations'] current_owner_or_empty = ticket._old.get('owner', ticket['owner']) current_owner = current_owner_or_empty or '(none)' if not (Chrome(self.env).show_email_addresses or 'EMAIL_VIEW' in req.perm(ticket.resource)): format_user = obfuscate_email_address else: format_user = lambda address: address current_owner = format_user(current_owner) control = [] # default to nothing hints = [] if 'reset_workflow' in operations: control.append(tag("from invalid state ")) hints.append(_("Current state no longer exists")) if 'del_owner' in operations: hints.append(_("The ticket will be disowned")) if 'set_owner' in operations or 'may_set_owner' in operations: if 'set_owner' in operations: default_owner = req.authname elif 'may_set_owner' in operations: default_owner = \ ticket._old.get('owner', ticket['owner'] or None) else: # Protect against future modification for case that another # operation is added to the outer conditional raise AssertionError(operations) if 'set_owner' in this_action: owners = [x.strip() for x in this_action['set_owner'].split(',')] elif self.config.getbool('ticket', 'restrict_owner'): perm = PermissionSystem(self.env) owners = perm.get_users_with_permission('TICKET_MODIFY') owners.sort() else: owners = None if owners is not None and default_owner not in owners: owners.insert(0, default_owner) id = 'action_%s_reassign_owner' % action selected_owner = req.args.get(id, default_owner) if owners is None: control.append( tag_('to %(owner)s', owner=tag.input(type='text', id=id, name=id, value=selected_owner))) hints.append(_("The owner will be changed from " "%(current_owner)s to the specified user", current_owner=current_owner)) elif len(owners) == 1: owner = tag.input(type='hidden', id=id, name=id, value=owners[0]) formatted_owner = format_user(owners[0]) control.append(tag_('to %(owner)s ', owner=tag(formatted_owner, owner))) if ticket['owner'] != owners[0]: hints.append(_("The owner will be changed from " "%(current_owner)s to %(selected_owner)s", current_owner=current_owner, selected_owner=formatted_owner)) else: control.append(tag_('to %(owner)s', owner=tag.select( [tag.option(x if x is not None else '(none)', value=x if x is not None else '', selected=(x == selected_owner or None)) for x in owners], id=id, name=id))) hints.append(_("The owner will be changed from " "%(current_owner)s to the selected user", current_owner=current_owner)) elif 'set_owner_to_self' in operations and \ ticket._old.get('owner', ticket['owner']) != req.authname: hints.append(_("The owner will be changed from %(current_owner)s " "to %(authname)s", current_owner=current_owner, authname=req.authname)) if 'set_resolution' in operations: if 'set_resolution' in this_action: resolutions = [x.strip() for x in this_action['set_resolution'].split(',')] else: resolutions = [val.name for val in Resolution.select(self.env)] if not resolutions: raise TracError(_("Your workflow attempts to set a resolution " "but none is defined (configuration issue, " "please contact your Trac admin).")) id = 'action_%s_resolve_resolution' % action if len(resolutions) == 1: resolution = tag.input(type='hidden', id=id, name=id, value=resolutions[0]) control.append(tag_('as %(resolution)s', resolution=tag(resolutions[0], resolution))) hints.append(_("The resolution will be set to %(name)s", name=resolutions[0])) else: selected_option = req.args.get(id, TicketSystem(self.env).default_resolution) control.append(tag_('as %(resolution)s', resolution=tag.select( [tag.option(x, value=x, selected=(x == selected_option or None)) for x in resolutions], id=id, name=id))) hints.append(_("The resolution will be set")) if 'del_resolution' in operations: hints.append(_("The resolution will be deleted")) if 'leave_status' in operations: control.append(_('as %(status)s ', status=ticket._old.get('status', ticket['status']))) if len(operations) == 1: hints.append(_("The owner will remain %(current_owner)s", current_owner=current_owner) if current_owner_or_empty else _("The ticket will remain with no owner")) else: if status != '*': hints.append(_("Next status will be '%(name)s'", name=status)) return (this_action['name'], tag(*control), '. '.join(hints) + '.' if hints else '')
def _render_admin_panel(self, req, cat, page, component): # Detail view? if component: comp = model.Component(self.env, component) if req.method == 'POST': if req.args.get('save'): comp.name = name = req.args.get('name') comp.owner = req.args.get('owner') comp.description = req.args.get('description') try: comp.update() except self.env.db_exc.IntegrityError: raise TracError( _('Component "%(name)s" already ' 'exists.', name=name)) add_notice(req, _("Your changes have been saved.")) req.redirect(req.href.admin(cat, page)) elif req.args.get('cancel'): req.redirect(req.href.admin(cat, page)) Chrome(self.env).add_wiki_toolbars(req) data = {'view': 'detail', 'component': comp} else: default = self.config.get('ticket', 'default_component') if req.method == 'POST': # Add Component if req.args.get('add') and req.args.get('name'): name = req.args.get('name') try: comp = model.Component(self.env, name=name) except ResourceNotFound: comp = model.Component(self.env) comp.name = name if req.args.get('owner'): comp.owner = req.args.get('owner') comp.insert() add_notice( req, _('The component "%(name)s" has been ' 'added.', name=name)) req.redirect(req.href.admin(cat, page)) else: if comp.name is None: raise TracError(_("Invalid component name.")) raise TracError( _('Component "%(name)s" already ' 'exists.', name=name)) # Remove components elif req.args.get('remove'): sel = req.args.getlist('sel') if not sel: raise TracError(_("No component selected")) with self.env.db_transaction: for name in sel: model.Component(self.env, name).delete() if name == default: self.config.set('ticket', 'default_component', '') self._save_config(req) add_notice( req, _("The selected components have been " "removed.")) req.redirect(req.href.admin(cat, page)) # Set default component elif req.args.get('apply'): name = req.args.get('default') if name and name != default: self.log.info("Setting default component to %s", name) self.config.set('ticket', 'default_component', name) self._save_config(req) req.redirect(req.href.admin(cat, page)) # Clear default component elif req.args.get('clear'): self.log.info("Clearing default component") self.config.set('ticket', 'default_component', '') self._save_config(req) req.redirect(req.href.admin(cat, page)) data = { 'view': 'list', 'components': list(model.Component.select(self.env)), 'default': default } owners = TicketSystem(self.env).get_allowed_owners() if owners is not None: owners.insert(0, '') data.update({'owners': owners}) return 'admin_components.html', data
def process_request(self, req): self.log.debug('HTTP request: %s, method: %s, user: %s' % (req.path_info, req.method, req.authname)) if req.method != 'GET' and req.method != 'POST': return req.send([], content_type='application/json') board_id = None is_ticket_call = False match = self.request_regexp.match(req.path_info) if match: board_id = match.group('bid') is_ticket_call = match.group('ticket') is not None if not self.ticket_fields: self.ticket_fields = TicketSystem(self.env).get_ticket_fields() if board_id is None: meta_data = {} meta_data['ticketFields'] = self.ticket_fields return req.send(json.dumps(meta_data), content_type='application/json') arg_list = parse_arg_list(req.query_string) detailed_tickets = [] added_tickets = [] removed_tickets = [] for arg in arg_list: if arg[0] == 'detailed': detailed_tickets = self._parse_id_list(arg[1]) elif arg[0] == 'add': added_tickets = self._parse_id_list(arg[1]) elif arg[0] == 'remove': removed_tickets = self._parse_id_list(arg[1]) board = KanbanBoard(board_id, detailed_tickets, self.ticket_fields, self.env, self.log) added = 0 if len(added_tickets) > 0: added = board.add_tickets(added_tickets) removed = 0 if len(removed_tickets) > 0: removed = board.remove_tickets(removed_tickets) # We need to update board data to match (possibly changed) ticket states is_editable = 'WIKI_MODIFY' in req.perm and 'TICKET_MODIFY' in req.perm board.fix_ticket_columns(req, is_editable, added > 0 or removed > 0) if req.method == 'GET': return req.send(board.get_json(True, False), content_type='application/json') else: if is_ticket_call: ticket_data = json.loads(req.read()) is_new = 'id' not in ticket_data id = self.save_ticket(ticket_data, req.authname) if is_new: board.add_tickets([id]) else: board.update_tickets([id]) else: modified_tickets = [] column_data = json.loads(req.read()) for col in column_data: for ticket in col['tickets']: for key, value in ticket.items(): if key != 'id': self.save_ticket(ticket, req.authname) modified_tickets.append(ticket['id']) break board.update_columns(column_data) if modified_tickets: board.update_tickets(modified_tickets) board.fix_ticket_columns(req, True, True) return req.send(board.get_json(True, False), content_type='application/json')
def setUp(self): self.env = EnvironmentStub(default_data=True) self.perm = PermissionSystem(self.env) self.ticket_system = TicketSystem(self.env) self.req = Mock()
def expand_macro(self, formatter, name, content, realms=[]): """Evaluate macro call and render results. Calls from web-UI come with pre-processed realm selection. """ env = self.env req = formatter.req tag_system = TagSystem(env) all_realms = tag_system.get_taggable_realms() if not all_realms: # Tag providers are required, no result without at least one. return '' args, kw = parse_args(content) query = args and args[0].strip() or None if not realms: # Check macro arguments for realms (typical wiki macro call). realms = 'realm' in kw and kw['realm'].split('|') or [] if query: # Add realms from query expression. realms.extend(query_realms(query, all_realms)) # Remove redundant realm selection for performance. if set(realms) == all_realms: query = re.sub('(^|\W)realm:\S+(\W|$)', ' ', query).strip() if name == 'TagCloud': # Set implicit 'all tagged realms' as default. if not realms: realms = all_realms if query: all_tags = Counter() # Require per resource query including view permission checks. for resource, tags in tag_system.query(req, query): all_tags.update(tags) else: # Allow faster per tag query, side steps permission checks. all_tags = tag_system.get_all_tags(req, realms=realms) mincount = 'mincount' in kw and kw['mincount'] or None return self.render_cloud(req, all_tags, caseless_sort=self.caseless_sort, mincount=mincount, realms=realms) elif name == 'ListTagged': if content and _OBSOLETE_ARGS_RE.search(content): data = {'warning': 'obsolete_args'} else: data = {'warning': None} context = formatter.context # Use TagsQuery arguments (most likely wiki macro calls). cols = 'cols' in kw and kw['cols'] or self.default_cols format = 'format' in kw and kw['format'] or self.default_format if not realms: # Apply ListTagged defaults to macro call w/o realm. realms = list(set(all_realms) - set(self.exclude_realms)) if not realms: return '' query = '(%s) (%s)' % (query or '', ' or '.join( ['realm:%s' % (r) for r in realms])) query_result = tag_system.query(req, query) excludes = [ exc.strip() for exc in kw.get('exclude', '').split(':') if exc.strip() ] if excludes and query_result: filtered_result = [(resource, tags) for resource, tags in query_result if not any( fnmatchcase(resource.id, exc) for exc in excludes)] query_result = filtered_result if not query_result: return '' 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') if format == 'table': cols = [ col for col in cols.split('|') if col in self.supported_cols ] # Use available translations from Trac core. try: labels = TicketSystem(env).get_ticket_field_labels() labels['id'] = _('Id') except AttributeError: # Trac 0.11 neither has the attribute nor uses i18n. labels = {'id': 'Id', 'description': 'Description'} labels['realm'] = _('Realm') labels['tags'] = _('Tags') headers = [{'label': labels.get(col)} for col in cols] data.update({'cols': cols, 'headers': headers}) try: results = sorted( query_result, key=lambda r: embedded_numbers(to_unicode(r[0].id))) except (InvalidQuery, InvalidTagRealm), e: return system_message(_("ListTagged macro error"), e) results = self._paginate(req, results, realms) rows = [] for resource, tags in results: desc = tag_system.describe_tagged_resource(req, resource) tags = sorted(tags) wiki_desc = format_to_oneliner(env, context, desc) if tags: rendered_tags = [ _link(Resource('tag', tag)) for tag in tags ] if 'oldlist' == format: resource_link = _link(resource) else: resource_link = builder.a(wiki_desc, href=get_resource_url( env, resource, context.href)) if 'table' == format: cells = [] for col in cols: if col == 'id': cells.append(_link(resource)) # Don't duplicate links to resource in both. elif col == 'description' and 'id' in cols: cells.append(wiki_desc) elif col == 'description': cells.append(resource_link) elif col == 'realm': cells.append(resource.realm) elif col == 'tags': cells.append( builder([(tag, ' ') for tag in rendered_tags])) rows.append({'cells': cells}) continue rows.append({ 'desc': wiki_desc, 'rendered_tags': None, 'resource_link': _link(resource) }) data.update({ 'format': format, 'paginator': results, 'results': rows, 'tags_url': req.href('tags') }) # Work around a bug in trac/templates/layout.html, that causes a # TypeError for the wiki macro call, if we use add_link() alone. add_stylesheet(req, 'common/css/search.css') return Chrome(env).render_template(req, 'listtagged_results.html', data, 'text/html', True)
class RestrictOwnerTestCase(unittest.TestCase): def setUp(self): tmpdir = os.path.realpath(tempfile.gettempdir()) self.env = EnvironmentStub(enable=['trac.*', AuthzPolicy], path=tmpdir) self.env.config.set('trac', 'permission_policies', 'AuthzPolicy, DefaultPermissionPolicy') self.env.config.set('ticket', 'restrict_owner', True) self.perm_sys = PermissionSystem(self.env) self.env.insert_users([('user1', 'User C', '*****@*****.**'), ('user2', 'User A', '*****@*****.**'), ('user3', 'User D', '*****@*****.**'), ('user4', 'User B', '*****@*****.**')]) self.perm_sys.grant_permission('user1', 'TICKET_MODIFY') self.perm_sys.grant_permission('user2', 'TICKET_VIEW') self.perm_sys.grant_permission('user3', 'TICKET_MODIFY') self.perm_sys.grant_permission('user4', 'TICKET_MODIFY') self.authz_file = os.path.join(tmpdir, 'trac-authz-policy') create_file(self.authz_file) self.env.config.set('authz_policy', 'authz_file', self.authz_file) self.ctlr = TicketSystem(self.env).action_controllers[0] self.req1 = MockRequest(self.env, authname='user1') self.ticket = Ticket(self.env) self.ticket['status'] = 'new' self.ticket.insert() def tearDown(self): self.env.reset_db() os.remove(self.authz_file) def _reload_workflow(self): self.ctlr.actions = self.ctlr.get_all_actions() def test_set_owner(self): """Restricted owners list contains users with TICKET_MODIFY. """ self.env.config.set('trac', 'show_full_names', False) ctrl = self.ctlr.render_ticket_action_control(self.req1, self.ticket, 'reassign') self.assertEqual('reassign', ctrl[0]) self.assertIn('value="user1">user1</option>', str(ctrl[1])) self.assertNotIn('value="user2">user2</option>', str(ctrl[1])) self.assertIn('value="user3">user3</option>', str(ctrl[1])) self.assertIn('value="user4">user4</option>', str(ctrl[1])) def test_set_owner_fine_grained_permissions(self): """Fine-grained permission checks when populating the restricted owners list (#10833). """ self.env.config.set('trac', 'show_full_names', False) create_file(self.authz_file, """\ [ticket:1] user4 = !TICKET_MODIFY """) ctrl = self.ctlr.render_ticket_action_control(self.req1, self.ticket, 'reassign') self.assertEqual('reassign', ctrl[0]) self.assertIn('value="user1">user1</option>', str(ctrl[1])) self.assertNotIn('value="user2">user2</option>', str(ctrl[1])) self.assertIn('value="user3">user3</option>', str(ctrl[1])) self.assertNotIn('value="user4">user4</option>', str(ctrl[1])) def test_set_owner_show_fullnames(self): """Full names are sorted when [trac] show_full_names = True.""" ctrl = self.ctlr.render_ticket_action_control(self.req1, self.ticket, 'reassign') self.assertEqual('reassign', ctrl[0]) self.assertEqual("""\ to <select name="action_reassign_reassign_owner" \ id="action_reassign_reassign_owner">\ <option value="user4">User B</option>\ <option selected="True" value="user1">User C</option>\ <option value="user3">User D</option></select>\ """, str(ctrl[1]))
def getAll(self, req): """ Returns all ticket states described by active workflow. """ return TicketSystem(self.env).get_all_status()
def get_configurable_workflow(self): controllers = TicketSystem(self.env).action_controllers for controller in controllers: if isinstance(controller, ConfigurableTicketWorkflow): return controller return ConfigurableTicketWorkflow(self.env)
def getTicketFields(self, req): """ Return a list of all ticket fields fields. """ return TicketSystem(self.env).get_ticket_fields()
def _get_ticket_field(self, field_name): fields = TicketSystem(self.env).get_ticket_fields() return next((i for i in fields if i['name'] == field_name))
def _render_admin_panel(self, req, cat, page, component): # Detail view? if component: comp = model.Component(self.env, component) if req.method == 'POST': if req.args.get('save'): comp.name = name = req.args.get('name') comp.owner = req.args.get('owner') comp.description = req.args.get('description') try: comp.update() except self.env.db_exc.IntegrityError: raise TracError(_('Component "%(name)s" already ' 'exists.', name=name)) add_notice(req, _('Your changes have been saved.')) req.redirect(req.href.admin(cat, page)) elif req.args.get('cancel'): req.redirect(req.href.admin(cat, page)) Chrome(self.env).add_wiki_toolbars(req) data = {'view': 'detail', 'component': comp} else: default = self.config.get('ticket', 'default_component') if req.method == 'POST': # Add Component if req.args.get('add') and req.args.get('name'): name = req.args.get('name') try: comp = model.Component(self.env, name=name) except ResourceNotFound: comp = model.Component(self.env) comp.name = name if req.args.get('owner'): comp.owner = req.args.get('owner') comp.insert() add_notice(req, _('The component "%(name)s" has been ' 'added.', name=name)) req.redirect(req.href.admin(cat, page)) else: if comp.name is None: raise TracError(_("Invalid component name.")) raise TracError(_('Component "%(name)s" already ' 'exists.', name=name)) # Remove components elif req.args.get('remove'): sel = req.args.get('sel') if not sel: raise TracError(_('No component selected')) if not isinstance(sel, list): sel = [sel] with self.env.db_transaction: for name in sel: model.Component(self.env, name).delete() add_notice(req, _("The selected components have been " "removed.")) req.redirect(req.href.admin(cat, page)) # Set default component elif req.args.get('apply'): name = req.args.get('default') if name and name != default: self.log.info("Setting default component to %s", name) self.config.set('ticket', 'default_component', name) _save_config(self.config, req, self.log) req.redirect(req.href.admin(cat, page)) data = {'view': 'list', 'components': list(model.Component.select(self.env)), 'default': default} owners = TicketSystem(self.env).get_allowed_owners() if owners is not None: owners.insert(0, '') data.update({'owners': owners}) return 'admin_components.html', data
def _get_actions(self, ticket_dict): ts = TicketSystem(self.env) ticket = insert_ticket(self.env, **ticket_dict) return ts.get_available_actions(self.req, Ticket(self.env, ticket.id))
def update(self, req, id, comment, attributes={}, notify=False, author='', when=None): """ Update a ticket, returning the new ticket in the same form as get(). 'New-style' call requires two additional items in attributes: (1) 'action' for workflow support (including any supporting fields as retrieved by getActions()), (2) '_ts' changetime token for detecting update collisions (as received from get() or update() calls). ''Calling update without 'action' and '_ts' changetime token is deprecated, and will raise errors in a future version.'' """ t = model.Ticket(self.env, id) # custom author? if author and not (req.authname == 'anonymous' \ or 'TICKET_ADMIN' in req.perm(t.resource)): # only allow custom author if anonymous is permitted or user is admin self.log.warn( "RPC ticket.update: %r not allowed to change author " "to %r for comment on #%d", req.authname, author, id) author = '' author = author or req.authname # custom change timestamp? if when and not 'TICKET_ADMIN' in req.perm(t.resource): self.log.warn( "RPC ticket.update: %r not allowed to update #%d with " "non-current timestamp (%r)", author, id, when) when = None when = when or to_datetime(None, utc) # never try to update 'time' and 'changetime' attributes directly if 'time' in attributes: del attributes['time'] if 'changetime' in attributes: del attributes['changetime'] # and action... if not 'action' in attributes: # FIXME: Old, non-restricted update - remove soon! self.log.warning("Rpc ticket.update for ticket %d by user %s " \ "has no workflow 'action'." % (id, req.authname)) req.perm(t.resource).require('TICKET_MODIFY') time_changed = attributes.pop('_ts', None) if time_changed and \ str(time_changed) != str(to_utimestamp(t.time_changed)): raise TracError("Ticket has been updated since last get().") for k, v in attributes.iteritems(): t[k] = v t.save_changes(author, comment, when=when) else: ts = TicketSystem(self.env) tm = TicketModule(self.env) # TODO: Deprecate update without time_changed timestamp time_changed = attributes.pop('_ts', to_utimestamp(t.time_changed)) try: time_changed = int(time_changed) except ValueError: raise TracError("RPC ticket.update: Wrong '_ts' token " \ "in attributes (%r)." % time_changed) action = attributes.get('action') avail_actions = ts.get_available_actions(req, t) if not action in avail_actions: raise TracError("Rpc: Ticket %d by %s " \ "invalid action '%s'" % (id, req.authname, action)) controllers = list(tm._get_action_controllers(req, t, action)) all_fields = [field['name'] for field in ts.get_ticket_fields()] for k, v in attributes.iteritems(): if k in all_fields and k != 'status': t[k] = v # TicketModule reads req.args - need to move things there... req.args.update(attributes) req.args['comment'] = comment # Collision detection: 0.11+0.12 timestamp req.args['ts'] = str(from_utimestamp(time_changed)) # Collision detection: 0.13/1.0+ timestamp req.args['view_time'] = str(time_changed) changes, problems = tm.get_ticket_changes(req, t, action) for warning in problems: add_warning(req, "Rpc ticket.update: %s" % warning) valid = problems and False or tm._validate_ticket(req, t) if not valid: raise TracError(" ".join( [warning for warning in req.chrome['warnings']])) else: tm._apply_ticket_changes(t, changes) self.log.debug("Rpc ticket.update save: %s" % repr(t.values)) t.save_changes(author, comment, when=when) # Apply workflow side-effects for controller in controllers: controller.apply_action_side_effects(req, t, action) if notify: try: tn = TicketNotifyEmail(self.env) tn.notify(t, newticket=False, modtime=when) except Exception, e: self.log.exception("Failure sending notification on change of " "ticket #%s: %s" % (t.id, e))
def setUp(self): self.env = EnvironmentStub(default_data=True) self.perm = PermissionSystem(self.env) self.ticket_system = TicketSystem(self.env) self.req = MockRequest(self.env)
def setUp(self): self.env = EnvironmentStub() self.perm = PermissionSystem(self.env) self.ticket_system = TicketSystem(self.env) self.req = Mock()
class SetOwnerAttributeTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True) self.perm_sys = PermissionSystem(self.env) self.ctlr = TicketSystem(self.env).action_controllers[0] self.ticket = Ticket(self.env) self.ticket['status'] = 'new' self.ticket.insert() with self.env.db_transaction as db: for user in ('user1', 'user2', 'user3', 'user4'): db("INSERT INTO session VALUES (%s, %s, %s)", (user, 1, 0)) permissions = [ ('user1', 'TICKET_EDIT_CC'), ('user2', 'TICKET_EDIT_CC'), ('user2', 'TICKET_BATCH_MODIFY'), ('user3', 'TICKET_ADMIN'), ('user4', 'TICKET_VIEW'), ('user1', 'group1'), ('user2', 'group1'), ('user2', 'group2'), ('user3', 'group2'), ('user4', 'group3') ] for perm in permissions: self.perm_sys.grant_permission(*perm) self.req = Mock(authname='user1', args={}, perm=PermissionCache(self.env, 'user0')) self.expected = """\ to <select name="action_reassign_reassign_owner" \ id="action_reassign_reassign_owner"><option selected="True" \ value="user1">user1</option><option value="user2">user2</option>\ <option value="user3">user3</option></select>""" def _reload_workflow(self): self.ctlr.actions = self.ctlr.get_all_actions() def tearDown(self): self.env.reset_db() def test_users(self): self.env.config.set('ticket-workflow', 'reassign.set_owner', 'user1, user2, user3') self._reload_workflow() args = self.req, self.ticket, 'reassign' label, control, hints = self.ctlr.render_ticket_action_control(*args) self.assertEqual(self.expected, str(control)) def test_groups(self): self.env.config.set('ticket-workflow', 'reassign.set_owner', 'group1, group2') self._reload_workflow() args = self.req, self.ticket, 'reassign' label, control, hints = self.ctlr.render_ticket_action_control(*args) self.assertEqual(self.expected, str(control)) def test_permission(self): self.env.config.set('ticket-workflow', 'reassign.set_owner', 'TICKET_EDIT_CC, TICKET_BATCH_MODIFY') self._reload_workflow() args = self.req, self.ticket, 'reassign' label, control, hints = self.ctlr.render_ticket_action_control(*args) self.assertEqual(self.expected, str(control))
def ticket_changed(self, ticket, comment, author, old_values): link_fields = [f['name'] for f in ticket.fields if f.get('link')] ticket_system = TicketSystem(self.env) links_provider = LinksProvider(self.env) remote_tktsys = RemoteTicketSystem(self.env) # We go behind trac's back to augment the ticket with remote links # As a result trac doesn't provide a correct old_values so fetch # our own orig_old_vals = old_values if old_values is None: old_values = {} else: self._augment_values(ticket.id, old_values) @self.env.with_transaction() def do_changed(db): cursor = db.cursor() for end in link_fields: # Determine links added or removed in this change by taking the # set difference of new and old values new_rtkts = set(remote_tktsys.parse_links(ticket[end])) old_rtkts = set(remote_tktsys.parse_links(old_values.get(end))) links_added = new_rtkts - old_rtkts links_removed = old_rtkts - new_rtkts links_changed = old_rtkts ^ new_rtkts # Additons and removals other_end = ticket_system.link_ends_map[end] # Add link records for remote links created in this change records = [('', ticket.id, end, rname, rid) for rname, rid in links_added] if other_end: records += [(rname, rid, other_end, '', ticket.id) for rname, rid in links_added] cursor.executemany( ''' INSERT INTO remote_ticket_links (source_name, source, type, destination_name, destination) VALUES (%s, %s, %s, %s, %s)''', records) # Remove link records for remote links removed in this change records = [('', ticket.id, end, rname, rid) for rname, rid in links_removed] if other_end: records += [(rname, rid, other_end, '', ticket.id) for rname, rid in links_added] cursor.executemany( ''' DELETE FROM remote_ticket_links WHERE source_name=%s AND source=%s AND type=%s AND destination_name=%s AND destination=%s''', records) # Record change history in ticket_change # Again we're going behind trac's back, so take care not to # obliterate existing records: # - If the field (end) has changed local links, as well as # changed remote links then update the record # - If the only change was to remote links then there is no # ticket_change record to update, so insert one if links_changed and orig_old_vals is not None: when_ts = to_utimestamp(ticket['changetime']) cursor.execute( ''' UPDATE ticket_change SET oldvalue=%s, newvalue=%s WHERE ticket=%s AND time=%s AND author=%s AND field=%s ''', (old_values[end], ticket[end], ticket.id, when_ts, author, end)) # Check that a row was updated, if so if cursor.rowcount >= 1: continue cursor.execute( ''' INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue) VALUES (%s, %s, %s, %s, %s, %s) ''', (ticket.id, when_ts, author, end, old_values[end], ticket[end]))
class ConfigurableTicketWorkflowTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True) config = self.env.config config.set('ticket-workflow', 'change_owner', 'new -> new') config.set('ticket-workflow', 'change_owner.operations', 'set_owner') self.ctlr = TicketSystem(self.env).action_controllers[0] self.ticket_module = TicketModule(self.env) def tearDown(self): self.env.reset_db() def _create_request(self, authname='anonymous', **kwargs): kw = {'path_info': '/', 'perm': MockPerm(), 'args': {}, 'href': self.env.href, 'abs_href': self.env.abs_href, 'tz': utc, 'locale': None, 'lc_time': locale_en, 'session': {}, 'authname': authname, 'chrome': {'notices': [], 'warnings': []}, 'method': None, 'get_header': lambda v: None, 'is_xhr': False, 'form_token': None, } kw.update(kwargs) def redirect(url, permanent=False): raise RequestDone return Mock(add_redirect_listener=lambda x: [].append(x), redirect=redirect, **kw) def _add_component(self, name='test', owner='owner1'): component = Component(self.env) component.name = name component.owner = owner component.insert() def test_get_all_actions_custom_attribute(self): """Custom attribute in ticket-workflow.""" config = self.env.config['ticket-workflow'] config.set('resolve.set_milestone', 'reject') all_actions = self.ctlr.get_all_actions() resolve_action = None for name, attrs in all_actions.items(): if name == 'resolve': resolve_action = attrs self.assertIsNotNone(resolve_action) self.assertIn('set_milestone', resolve_action.keys()) self.assertEqual('reject', resolve_action['set_milestone']) 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 test_component_change(self): """New ticket owner is updated when the component is changed. """ self._add_component('component3', 'cowner3') self._add_component('component4', 'cowner4') ticket = Ticket(self.env) ticket.populate({ 'reporter': 'reporter1', 'summary': 'the summary', 'component': 'component3', 'owner': 'cowner3', 'status': 'new', }) tkt_id = ticket.insert() req = self._create_request(method='POST', args={ 'id': tkt_id, 'field_component': 'component4', 'submit': True, 'action': 'leave', 'view_time': str(to_utimestamp(ticket['changetime'])), }) self.assertRaises(RequestDone, self.ticket_module.process_request, req) ticket = Ticket(self.env, tkt_id) self.assertEqual('component4', ticket['component']) self.assertEqual('cowner4', ticket['owner']) def test_component_change_and_owner_change(self): """New ticket owner is not updated if owner is explicitly changed. """ self._add_component('component3', 'cowner3') self._add_component('component4', 'cowner4') ticket = Ticket(self.env) ticket.populate({ 'reporter': 'reporter1', 'summary': 'the summary', 'component': 'component3', 'status': 'new', }) tkt_id = ticket.insert() req = self._create_request(method='POST', args={ 'id': tkt_id, 'field_component': 'component4', 'submit': True, 'action': 'change_owner', 'action_change_owner_reassign_owner': 'owner1', 'view_time': str(to_utimestamp(ticket['changetime'])), }) self.assertRaises(RequestDone, self.ticket_module.process_request, req) ticket = Ticket(self.env, tkt_id) self.assertEqual('component4', ticket['component']) self.assertEqual('owner1', ticket['owner']) def test_old_owner_not_old_component_owner(self): """New ticket owner is not updated if old owner is not the owner of the old component. """ self._add_component('component3', 'cowner3') self._add_component('component4', 'cowner4') ticket = Ticket(self.env) ticket.populate({ 'reporter': 'reporter1', 'summary': 'the summary', 'component': 'component3', 'owner': 'owner1', 'status': 'new', }) tkt_id = ticket.insert() req = self._create_request(method='POST', args={ 'id': tkt_id, 'field_component': 'component4', 'submit': True, 'action': 'leave', 'view_time': str(to_utimestamp(ticket['changetime'])), }) self.assertRaises(RequestDone, self.ticket_module.process_request, req) ticket = Ticket(self.env, tkt_id) self.assertEqual('component4', ticket['component']) self.assertEqual('owner1', ticket['owner']) def test_new_component_has_no_owner(self): """Ticket is not disowned when the component is changed to a component with no owner. """ self._add_component('component3', 'cowner3') self._add_component('component4', '') ticket = Ticket(self.env) ticket.populate({ 'reporter': 'reporter1', 'summary': 'the summary', 'component': 'component3', 'owner': 'cowner3', 'status': 'new', }) tkt_id = ticket.insert() req = self._create_request(method='POST', args={ 'id': tkt_id, 'field_component': 'component4', 'submit': True, 'action': 'leave', 'view_time': str(to_utimestamp(ticket['changetime'])), }) self.assertRaises(RequestDone, self.ticket_module.process_request, req) ticket = Ticket(self.env, tkt_id) self.assertEqual('component4', ticket['component']) self.assertEqual('cowner3', ticket['owner'])
class RestrictOwnerTestCase(unittest.TestCase): def setUp(self): tmpdir = os.path.realpath(tempfile.gettempdir()) self.env = EnvironmentStub(enable=['trac.*', AuthzPolicy], path=tmpdir) self.env.config.set('trac', 'permission_policies', 'AuthzPolicy, DefaultPermissionPolicy') self.env.config.set('ticket', 'restrict_owner', True) self.perm_sys = PermissionSystem(self.env) self.env.insert_known_users([ ('user1', '', ''), ('user2', '', ''), ('user3', '', ''), ('user4', '', '') ]) self.perm_sys.grant_permission('user1', 'TICKET_MODIFY') self.perm_sys.grant_permission('user2', 'TICKET_VIEW') self.perm_sys.grant_permission('user3', 'TICKET_MODIFY') self.perm_sys.grant_permission('user4', 'TICKET_MODIFY') self.authz_file = os.path.join(tmpdir, 'trac-authz-policy') create_file(self.authz_file) self.env.config.set('authz_policy', 'authz_file', self.authz_file) self.ctlr = TicketSystem(self.env).action_controllers[0] self.req1 = Mock(authname='user1', args={}, perm=PermissionCache(self.env, 'user1')) self.ticket = Ticket(self.env) self.ticket['status'] = 'new' self.ticket.insert() def tearDown(self): self.env.reset_db() os.remove(self.authz_file) def _reload_workflow(self): self.ctlr.actions = self.ctlr.get_all_actions() def test_set_owner(self): """Restricted owners list contains users with TICKET_MODIFY. """ ctrl = self.ctlr.render_ticket_action_control(self.req1, self.ticket, 'reassign') self.assertEqual('reassign', ctrl[0]) self.assertIn('value="user1">user1</option>', str(ctrl[1])) self.assertNotIn('value="user2">user2</option>', str(ctrl[1])) self.assertIn('value="user3">user3</option>', str(ctrl[1])) self.assertIn('value="user4">user4</option>', str(ctrl[1])) def test_set_owner_fine_grained_permissions(self): """Fine-grained permission checks when populating the restricted owners list (#10833). """ create_file(self.authz_file, """\ [ticket:1] user4 = !TICKET_MODIFY """) ctrl = self.ctlr.render_ticket_action_control(self.req1, self.ticket, 'reassign') self.assertEqual('reassign', ctrl[0]) self.assertIn('value="user1">user1</option>', str(ctrl[1])) self.assertNotIn('value="user2">user2</option>', str(ctrl[1])) self.assertIn('value="user3">user3</option>', str(ctrl[1])) self.assertNotIn('value="user4">user4</option>', str(ctrl[1]))
class SetOwnerAttributeTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True) self.perm_sys = PermissionSystem(self.env) self.ctlr = TicketSystem(self.env).action_controllers[0] self.ticket = Ticket(self.env) self.ticket['status'] = 'new' self.ticket.insert() with self.env.db_transaction as db: for user in ('user1', 'user2', 'user3', 'user4'): db("INSERT INTO session VALUES (%s, %s, %s)", (user, 1, 0)) permissions = [ ('user1', 'TICKET_EDIT_CC'), ('user2', 'TICKET_EDIT_CC'), ('user2', 'TICKET_BATCH_MODIFY'), ('user3', 'TICKET_ADMIN'), ('user4', 'TICKET_VIEW'), ('user1', 'group1'), ('user2', 'group1'), ('user2', 'group2'), ('user3', 'group2'), ('user4', 'group3') ] for perm in permissions: self.perm_sys.grant_permission(*perm) self.req = MockRequest(self.env, authname='user1') self.expected = """\ to <select name="action_reassign_reassign_owner" \ id="action_reassign_reassign_owner"><option selected="True" \ value="user1">user1</option><option value="user2">user2</option>\ <option value="user3">user3</option></select>""" def _reload_workflow(self): self.ctlr.actions = self.ctlr.get_all_actions() def tearDown(self): self.env.reset_db() def test_users(self): self.env.config.set('ticket-workflow', 'reassign.set_owner', 'user1, user2, user3') self._reload_workflow() args = self.req, self.ticket, 'reassign' label, control, hints = self.ctlr.render_ticket_action_control(*args) self.assertEqual(self.expected, str(control)) def test_groups(self): self.env.config.set('ticket-workflow', 'reassign.set_owner', 'group1, group2') self._reload_workflow() args = self.req, self.ticket, 'reassign' label, control, hints = self.ctlr.render_ticket_action_control(*args) self.assertEqual(self.expected, str(control)) def test_permission(self): self.env.config.set('ticket-workflow', 'reassign.set_owner', 'TICKET_EDIT_CC, TICKET_BATCH_MODIFY') self._reload_workflow() args = self.req, self.ticket, 'reassign' label, control, hints = self.ctlr.render_ticket_action_control(*args) self.assertEqual(self.expected, str(control))
def expand_macro(self, formatter, name, content, args=[]): try: cols = [] # Sentinel group = '' # Sentinel groups = {} lines = content.split('\r\n') for line in lines: if line.startswith('||= href =||= '): cols = line[14:].split(' =||= ') elif line.startswith('|| group: '): group = line[10:] if group in [u'', u'None']: group = None groups[group] = [] # initialize for the group elif line.startswith('|| '): values = iter(line[3:].split(' || ')) ticket = {'href': values.next()} for col in cols: ticket[col] = values.next() groups[group].append(ticket) else: pass ticketsystem = TicketSystem(self.env) # labels = ticketsystem.get_ticket_field_labels() headers = [{ 'name': col, 'label': labels.get(col, _('Ticket')) } for col in cols] # fields = {} ticket_fields = ticketsystem.get_ticket_fields() for field in ticket_fields: fields[field['name']] = { 'label': field['label'] } # transform list to expected dict # fail safe fields[None] = 'NONE' for group in groups.keys(): if not 'group' in fields: fields[group] = group # group_name = 'group' in args and args['group'] or None if group_name not in fields: group_name = None query = {'group': group_name} # groups = [(name, groups[name]) for name in groups] # transform dict to expected tuple # data = { 'paginator': None, 'headers': headers, 'query': query, 'fields': fields, 'groups': groups, } add_stylesheet(formatter.req, 'common/css/report.css') chrome = Chrome(self.env) data = chrome.populate_data(formatter.req, data) template = chrome.load_template('query_results.html') content = template.generate(**data) # ticket id list as static tickets = '' if 'id' in cols: ticket_id_list = [ ticket.get('id') for group in groups for ticket in group[1] ] if len(ticket_id_list) > 0: tickets = '([ticket:' + ','.join( ticket_id_list) + ' query by ticket id])' return tag.div( content, format_to_html(self.env, formatter.context, tickets)) except StopIteration: errorinfo = _('Not Enough fields in ticket: %s') % line except Exception: errorinfo = sys.exc_info() return tag.div(tag.div(errorinfo, class_='message'), class_='error', id='content')
def _render_view(self, req, milestone): milestone_groups = [] available_groups = [] component_group_available = False ticket_fields = TicketSystem(self.env).get_ticket_fields() # collect fields that can be used for grouping for field in ticket_fields: if field['type'] == 'select' and field['name'] != 'milestone' \ or field['name'] in ('owner', 'reporter'): available_groups.append({'name': field['name'], 'label': field['label']}) if field['name'] == 'component': component_group_available = True # determine the field currently used for grouping by = None if component_group_available: by = 'component' elif available_groups: by = available_groups[0]['name'] by = req.args.get('by', by) tickets = get_tickets_for_milestone(self.env, milestone=milestone.name, field=by) tickets = apply_ticket_permissions(self.env, req, tickets) stat = get_ticket_stats(self.stats_provider, tickets) context = web_context(req, milestone.resource) data = { 'context': context, 'milestone': milestone, 'attachments': AttachmentModule(self.env).attachment_data(context), 'available_groups': available_groups, 'grouped_by': by, 'groups': milestone_groups } data.update(milestone_stats_data(self.env, req, stat, milestone.name)) if by: def per_group_stats_data(gstat, group_name): return milestone_stats_data(self.env, req, gstat, milestone.name, by, group_name) milestone_groups.extend( grouped_stats_data(self.env, self.stats_provider, tickets, by, per_group_stats_data)) add_stylesheet(req, 'common/css/roadmap.css') add_script(req, 'common/js/folding.js') def add_milestone_link(rel, milestone): href = req.href.milestone(milestone.name, by=req.args.get('by')) add_link(req, rel, href, _('Milestone "%(name)s"', name=milestone.name)) milestones = [m for m in Milestone.select(self.env) if 'MILESTONE_VIEW' in req.perm(m.resource)] idx = [i for i, m in enumerate(milestones) if m.name == milestone.name] if idx: idx = idx[0] if idx > 0: add_milestone_link('first', milestones[0]) add_milestone_link('prev', milestones[idx - 1]) if idx < len(milestones) - 1: add_milestone_link('next', milestones[idx + 1]) add_milestone_link('last', milestones[-1]) prevnext_nav(req, _('Previous Milestone'), _('Next Milestone'), _('Back to Roadmap')) return 'milestone_view.html', data, None
class TicketSystemTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True) self.perm = PermissionSystem(self.env) self.ticket_system = TicketSystem(self.env) self.req = MockRequest(self.env) def tearDown(self): self.env.reset_db() def _get_actions(self, ticket_dict): ts = TicketSystem(self.env) ticket = insert_ticket(self.env, **ticket_dict) return ts.get_available_actions(self.req, Ticket(self.env, ticket.id)) def _get_ticket_field(self, field_name): fields = TicketSystem(self.env).get_ticket_fields() return next((i for i in fields if i['name'] == field_name)) def test_custom_field_text(self): self.env.config.set('ticket-custom', 'test', 'text') self.env.config.set('ticket-custom', 'test.label', 'Test') self.env.config.set('ticket-custom', 'test.value', 'Foo bar') self.env.config.set('ticket-custom', 'test.format', 'wiki') fields = TicketSystem(self.env).get_custom_fields() self.assertEqual( { 'name': 'test', 'type': 'text', 'label': 'Test', 'value': 'Foo bar', 'max_size': 0, 'order': 0, 'format': 'wiki', 'custom': True }, fields[0]) def test_custom_field_select(self): self.env.config.set('ticket-custom', 'test', 'select') self.env.config.set('ticket-custom', 'test.label', 'Test') self.env.config.set('ticket-custom', 'test.value', '1') self.env.config.set('ticket-custom', 'test.options', 'option1|option2') fields = TicketSystem(self.env).get_custom_fields() self.assertEqual( { 'name': 'test', 'type': 'select', 'label': 'Test', 'value': '1', 'options': ['option1', 'option2'], 'order': 0, 'custom': True }, fields[0]) def test_custom_field_optional_select(self): self.env.config.set('ticket-custom', 'test', 'select') self.env.config.set('ticket-custom', 'test.label', 'Test') self.env.config.set('ticket-custom', 'test.value', '1') self.env.config.set('ticket-custom', 'test.options', '|option1|option2') fields = TicketSystem(self.env).get_custom_fields() self.assertEqual( { 'name': 'test', 'type': 'select', 'label': 'Test', 'value': '1', 'options': ['option1', 'option2'], 'order': 0, 'optional': True, 'custom': True }, fields[0]) def test_custom_field_select_without_options(self): self.env.config.set('ticket-custom', 'test', 'select') self.env.config.set('ticket-custom', 'test.label', 'Test') self.env.config.set('ticket-custom', 'test.value', '1') fields = TicketSystem(self.env).get_custom_fields() self.assertEqual(0, len(fields)) def test_custom_field_textarea(self): self.env.config.set('ticket-custom', 'test', 'textarea') self.env.config.set('ticket-custom', 'test.label', 'Test') self.env.config.set('ticket-custom', 'test.value', 'Foo bar') self.env.config.set('ticket-custom', 'test.rows', '4') self.env.config.set('ticket-custom', 'test.format', 'wiki') fields = TicketSystem(self.env).get_custom_fields() self.assertEqual( { 'name': 'test', 'type': 'textarea', 'label': 'Test', 'value': 'Foo bar', 'height': 4, 'order': 0, 'max_size': 0, 'format': 'wiki', 'custom': True }, fields[0]) def test_description_field(self): field = self._get_ticket_field('description') self.assertEqual( { 'name': 'description', 'label': 'Description', 'type': 'textarea', 'format': 'wiki' }, field) def test_custom_field_checkbox(self): def add_checkbox(name, value): self.env.config.set('ticket-custom', name, 'checkbox') self.env.config.set('ticket-custom', '%s.value' % name, value) add_checkbox('checkbox0', 'true') add_checkbox('checkbox1', 1) add_checkbox('checkbox2', 'enabled') add_checkbox('checkbox3', 0) add_checkbox('checkbox4', 'tru') add_checkbox('checkbox5', 'off') fields = TicketSystem(self.env).get_custom_fields() self.assertEqual( { 'name': 'checkbox0', 'type': 'checkbox', 'label': 'Checkbox0', 'value': '1', 'order': 0, 'custom': True }, fields[0]) self.assertEqual('1', fields[1]['value']) self.assertEqual('1', fields[2]['value']) self.assertEqual('0', fields[3]['value']) self.assertEqual('0', fields[4]['value']) self.assertEqual('0', fields[5]['value']) def test_custom_field_time(self): self.env.config.set('ticket-custom', 'test', 'time') self.env.config.set('ticket-custom', 'test.label', 'Test') self.env.config.set('ticket-custom', 'test.value', '') fields = TicketSystem(self.env).get_custom_fields() self.assertEqual( { 'name': 'test', 'type': 'time', 'label': 'Test', 'value': '', 'order': 0, 'format': 'datetime', 'custom': True }, fields[0]) def test_custom_field_with_invalid_name(self): ticket_custom = self.env.config['ticket-custom'] ticket_custom.set('_field1', 'text') ticket_custom.set('2field', 'text') ticket_custom.set('f3%^&*', 'text') ticket_custom.set('field4', 'text') ticket_custom.set('FiEld5', 'text') ts = TicketSystem(self.env) custom_fields = ts.custom_fields fields = ts.fields self.assertEqual(2, len(custom_fields)) self.assertIsNotNone(custom_fields.by_name('field4')) self.assertIsNotNone(custom_fields.by_name('field5')) self.assertIsNotNone(fields.by_name('field4')) self.assertIsNotNone(fields.by_name('field5')) self.assertIn(('WARNING', u'Invalid name for custom field: "_field1" (ignoring)'), self.env.log_messages) self.assertIn( ('WARNING', u'Invalid name for custom field: "2field" (ignoring)'), self.env.log_messages) self.assertIn( ('WARNING', u'Invalid name for custom field: "f3%^&*" (ignoring)'), self.env.log_messages) def test_custom_field_with_reserved_name(self): ticket_custom = self.env.config['ticket-custom'] ticket_custom.set('owner', 'select') ticket_custom.set('owner.options', 'u1|u2|u3') ticket_custom.set('description', 'text') ts = TicketSystem(self.env) custom_fields = ts.custom_fields self.assertIn( ('WARNING', u'Field name "owner" is a reserved name (ignoring)'), self.env.log_messages) self.assertIn( ('WARNING', u'Field name "description" is a reserved name (ignoring)'), self.env.log_messages) self.assertEqual({ 'name': 'owner', 'label': 'Owner', 'type': 'text' }, ts.fields.by_name('owner')) self.assertEqual( { 'name': 'description', 'label': 'Description', 'type': 'textarea', 'format': 'wiki' }, ts.fields.by_name('description')) self.assertIsNone(custom_fields.by_name('owner')) self.assertIsNone(custom_fields.by_name('description')) def test_custom_field_order(self): self.env.config.set('ticket-custom', 'test1', 'text') self.env.config.set('ticket-custom', 'test1.order', '2') self.env.config.set('ticket-custom', 'test2', 'text') self.env.config.set('ticket-custom', 'test2.order', '1') fields = TicketSystem(self.env).get_custom_fields() self.assertEqual('test2', fields[0]['name']) self.assertEqual('test1', fields[1]['name']) def test_custom_field_label(self): self.env.config.set('ticket-custom', 'test_one', 'text') self.env.config.set('ticket-custom', 'test_two', 'text') self.env.config.set('ticket-custom', 'test_two.label', 'test_2') fields = TicketSystem(self.env).get_custom_fields() self.assertEqual('Test one', fields[0]['label']) self.assertEqual('test_2', fields[1]['label']) def _test_custom_field_with_enum(self, name, cls): tktsys = TicketSystem(self.env) instance = cls(self.env) instance.name = '%s 42' % name instance.insert() self.env.config.set('ticket-custom', name, 'text') field = self._get_ticket_field(name) self.assertFalse(field.get('custom')) with self.env.db_transaction: instances = list(cls.select(self.env)) if issubclass(cls, model.AbstractEnum): # delete from highest to lowest to avoid re-ordering enums instances.sort(reverse=True, key=lambda v: int(v.value)) for instance in instances: instance.delete() field = self._get_ticket_field(name) self.assertTrue(field.get('custom')) def test_custom_field_type(self): self._test_custom_field_with_enum('type', model.Type) def test_custom_field_priority(self): self._test_custom_field_with_enum('priority', model.Priority) def test_custom_field_milestone(self): self._test_custom_field_with_enum('milestone', Milestone) def test_custom_field_component(self): self._test_custom_field_with_enum('component', model.Component) def test_custom_field_version(self): self._test_custom_field_with_enum('version', Version) def test_custom_field_severity(self): self._test_custom_field_with_enum('severity', model.Severity) def test_custom_field_resolution(self): self._test_custom_field_with_enum('resolution', model.Resolution) def test_available_actions_full_perms(self): self.perm.grant_permission('anonymous', 'TICKET_CREATE') self.perm.grant_permission('anonymous', 'TICKET_MODIFY') self.req.perm = PermissionCache(self.env) self.assertEqual(['leave', 'resolve', 'reassign', 'accept'], self._get_actions({'status': 'new'})) self.assertEqual(['leave', 'resolve', 'reassign', 'accept'], self._get_actions({'status': 'assigned'})) self.assertEqual(['leave', 'resolve', 'reassign', 'accept'], self._get_actions({'status': 'accepted'})) self.assertEqual(['leave', 'resolve', 'reassign', 'accept'], self._get_actions({'status': 'reopened'})) self.assertEqual(['leave', 'reopen'], self._get_actions({'status': 'closed'})) def test_available_actions_no_perms(self): self.req.perm = PermissionCache(self.env) self.assertEqual(['leave'], self._get_actions({'status': 'new'})) self.assertEqual(['leave'], self._get_actions({'status': 'assigned'})) self.assertEqual(['leave'], self._get_actions({'status': 'accepted'})) self.assertEqual(['leave'], self._get_actions({'status': 'reopened'})) self.assertEqual(['leave'], self._get_actions({'status': 'closed'})) def test_available_actions_create_only(self): self.perm.grant_permission('anonymous', 'TICKET_CREATE') self.req.perm = PermissionCache(self.env) self.assertEqual(['leave'], self._get_actions({'status': 'new'})) self.assertEqual(['leave'], self._get_actions({'status': 'assigned'})) self.assertEqual(['leave'], self._get_actions({'status': 'accepted'})) self.assertEqual(['leave'], self._get_actions({'status': 'reopened'})) self.assertEqual(['leave', 'reopen'], self._get_actions({'status': 'closed'})) def test_available_actions_chgprop_only(self): # CHGPROP is not enough for changing a ticket's state (#3289) self.perm.grant_permission('anonymous', 'TICKET_CHGPROP') self.req.perm = PermissionCache(self.env) self.assertEqual(['leave'], self._get_actions({'status': 'new'})) self.assertEqual(['leave'], self._get_actions({'status': 'assigned'})) self.assertEqual(['leave'], self._get_actions({'status': 'accepted'})) self.assertEqual(['leave'], self._get_actions({'status': 'reopened'})) self.assertEqual(['leave'], self._get_actions({'status': 'closed'})) def test_get_allowed_owners_restrict_owner_false(self): self.env.config.set('ticket', 'restrict_owner', False) self.assertIsNone(self.ticket_system.get_allowed_owners()) def test_get_allowed_owners_restrict_owner_true(self): self.env.config.set('ticket', 'restrict_owner', True) self.env.insert_users([('user3', None, None), ('user1', None, None)]) self.perm.grant_permission('user4', 'TICKET_MODIFY') self.perm.grant_permission('user3', 'TICKET_MODIFY') self.perm.grant_permission('user2', 'TICKET_VIEW') self.perm.grant_permission('user1', 'TICKET_MODIFY') self.assertEqual(['user1', 'user3'], self.ticket_system.get_allowed_owners()) def test_get_ticket_fields_version_rename(self): """Cached ticket fields are updated when version is renamed.""" fields = self.ticket_system.get_ticket_fields() version_field = self._get_ticket_field('version') v2 = Version(self.env, '2.0') v2.name = '0.0' v2.update() updated_fields = self.ticket_system.get_ticket_fields() updated_version_field = self._get_ticket_field('version') self.assertNotEqual(fields, updated_fields) self.assertEqual(['2.0', '1.0'], version_field['options']) self.assertEqual(['1.0', '0.0'], updated_version_field['options']) def test_get_ticket_fields_version_update_time(self): """Cached ticket fields are updated when version release time is changed. """ fields = self.ticket_system.get_ticket_fields() version_field = self._get_ticket_field('version') v1 = Version(self.env, '1.0') v1.time = datetime_now(utc) v2 = Version(self.env, '2.0') v2.time = v1.time - timedelta(seconds=1) v1.update() v2.update() updated_fields = self.ticket_system.get_ticket_fields() updated_version_field = self._get_ticket_field('version') self.assertNotEqual(fields, updated_fields) self.assertEqual(['2.0', '1.0'], version_field['options']) self.assertEqual(['1.0', '2.0'], updated_version_field['options']) def test_get_ticket_fields_milestone_rename(self): """Cached ticket fields are updated when milestone is renamed.""" fields = self.ticket_system.get_ticket_fields() milestone_field = self._get_ticket_field('milestone') m2 = Milestone(self.env, 'milestone2') m2.name = 'milestone5' m2.update() updated_fields = self.ticket_system.get_ticket_fields() updated_milestone_field = self._get_ticket_field('milestone') self.assertNotEqual(fields, updated_fields) self.assertEqual( ['milestone1', 'milestone2', 'milestone3', 'milestone4'], milestone_field['options']) self.assertEqual( ['milestone1', 'milestone3', 'milestone4', 'milestone5'], updated_milestone_field['options']) def test_get_ticket_fields_milestone_update_completed(self): """Cached ticket fields are updated when milestone is completed date is changed. """ fields = self.ticket_system.get_ticket_fields() milestone_field = self._get_ticket_field('milestone') m2 = Milestone(self.env, 'milestone2') m2.completed = datetime_now(utc) m2.update() updated_fields = self.ticket_system.get_ticket_fields() updated_milestone_field = self._get_ticket_field('milestone') self.assertNotEqual(fields, updated_fields) self.assertEqual( ['milestone1', 'milestone2', 'milestone3', 'milestone4'], milestone_field['options']) self.assertEqual( ['milestone2', 'milestone1', 'milestone3', 'milestone4'], updated_milestone_field['options']) def test_get_ticket_fields_milestone_update_due(self): """Cached ticket fields are updated when milestone due date is changed. """ fields = self.ticket_system.get_ticket_fields() milestone_field = self._get_ticket_field('milestone') m2 = Milestone(self.env, 'milestone2') m2.due = datetime_now(utc) m2.update() updated_fields = self.ticket_system.get_ticket_fields() updated_milestone_field = self._get_ticket_field('milestone') self.assertNotEqual(fields, updated_fields) self.assertEqual( ['milestone1', 'milestone2', 'milestone3', 'milestone4'], milestone_field['options']) self.assertEqual( ['milestone2', 'milestone1', 'milestone3', 'milestone4'], updated_milestone_field['options']) def test_resource_exists_valid_resource_id(self): insert_ticket(self.env) r1 = Resource('ticket', 1) r2 = Resource('ticket', 2) self.assertTrue(self.ticket_system.resource_exists(r1)) self.assertFalse(self.ticket_system.resource_exists(r2)) def test_resource_exists_invalid_resource_id(self): """Exception is trapped from resource with invalid id.""" r1 = Resource('ticket', None) r2 = Resource('ticket', 'abc') r3 = Resource('ticket', '2.') r4 = Resource('ticket', r2) self.assertFalse(self.ticket_system.resource_exists(r1)) self.assertFalse(self.ticket_system.resource_exists(r2)) self.assertFalse(self.ticket_system.resource_exists(r3)) self.assertFalse(self.ticket_system.resource_exists(r4))
def render_ticket_action_control(self, req, ticket, action): self.log.debug('render_ticket_action_control: action "%s"', action) this_action = self.actions[action] status = this_action['newstate'] operations = this_action['operations'] current_owner = ticket._old.get('owner', ticket['owner']) author = get_reporter_id(req, 'author') author_info = partial(Chrome(self.env).authorinfo, req, resource=ticket.resource) format_author = partial(Chrome(self.env).format_author, req, resource=ticket.resource) formatted_current_owner = author_info(current_owner) exists = ticket._old.get('status', ticket['status']) is not None control = [] # default to nothing hints = [] if 'reset_workflow' in operations: control.append(_("from invalid state")) hints.append(_("Current state no longer exists")) if 'del_owner' in operations: hints.append(_("The ticket will be disowned")) if 'set_owner' in operations or 'may_set_owner' in operations: if 'set_owner' in this_action: owners = self._to_users(this_action['set_owner'], ticket) elif self.config.getbool('ticket', 'restrict_owner'): perm = PermissionSystem(self.env) owners = perm.get_users_with_permission('TICKET_MODIFY') owners = [ user for user in owners if 'TICKET_MODIFY' in PermissionCache( self.env, user, ticket.resource) ] owners = sorted(owners) else: owners = None if 'set_owner' in operations: default_owner = author elif 'may_set_owner' in operations: if not exists: default_owner = TicketSystem(self.env).default_owner else: default_owner = ticket._old.get('owner', ticket['owner'] or None) if owners is not None and default_owner not in owners: owners.insert(0, default_owner) else: # Protect against future modification for case that another # operation is added to the outer conditional raise AssertionError(operations) id = 'action_%s_reassign_owner' % action if not owners: owner = req.args.get(id, default_owner) control.append( tag_("to %(owner)s", owner=tag.input(type='text', id=id, name=id, value=owner))) if not exists or current_owner is None: hints.append(_("The owner will be the specified user")) else: hints.append( tag_( "The owner will be changed from " "%(current_owner)s to the specified " "user", current_owner=formatted_current_owner)) elif len(owners) == 1: owner = tag.input(type='hidden', id=id, name=id, value=owners[0]) formatted_new_owner = author_info(owners[0]) control.append( tag_("to %(owner)s", owner=tag(formatted_new_owner, owner))) if not exists or current_owner is None: hints.append( tag_("The owner will be %(new_owner)s", new_owner=formatted_new_owner)) elif ticket['owner'] != owners[0]: hints.append( tag_( "The owner will be changed from " "%(current_owner)s to %(new_owner)s", current_owner=formatted_current_owner, new_owner=formatted_new_owner)) else: selected_owner = req.args.get(id, default_owner) control.append( tag_("to %(owner)s", owner=tag.select([ tag.option( label, value=value if value is not None else '', selected=(value == selected_owner or None)) for label, value in sorted( (format_author(owner), owner) for owner in owners) ], id=id, name=id))) if not exists or current_owner is None: hints.append(_("The owner will be the selected user")) else: hints.append( tag_( "The owner will be changed from " "%(current_owner)s to the selected user", current_owner=formatted_current_owner)) elif 'set_owner_to_self' in operations and \ ticket._old.get('owner', ticket['owner']) != author: formatted_author = author_info(author) if not exists or current_owner is None: hints.append( tag_("The owner will be %(new_owner)s", new_owner=formatted_author)) else: hints.append( tag_( "The owner will be changed from " "%(current_owner)s to %(new_owner)s", current_owner=formatted_current_owner, new_owner=formatted_author)) if 'set_resolution' in operations: if 'set_resolution' in this_action: resolutions = this_action['set_resolution'] else: resolutions = [r.name for r in Resolution.select(self.env)] if not resolutions: raise TracError( _("Your workflow attempts to set a resolution " "but none is defined (configuration issue, " "please contact your Trac admin).")) id = 'action_%s_resolve_resolution' % action if len(resolutions) == 1: resolution = tag.input(type='hidden', id=id, name=id, value=resolutions[0]) control.append( tag_("as %(resolution)s", resolution=tag(resolutions[0], resolution))) hints.append( tag_("The resolution will be set to %(name)s", name=resolutions[0])) else: selected_option = req.args.get( id, TicketSystem(self.env).default_resolution) control.append( tag_( "as %(resolution)s", resolution=tag.select([ tag.option(x, value=x, selected=(x == selected_option or None)) for x in resolutions ], id=id, name=id))) hints.append(_("The resolution will be set")) if 'del_resolution' in operations: hints.append(_("The resolution will be deleted")) if 'leave_status' in operations: control.append( tag_("as %(status)s", status=ticket._old.get('status', ticket['status']))) if len(operations) == 1: hints.append( tag_("The owner will remain %(current_owner)s", current_owner=formatted_current_owner) if current_owner else _("The ticket will remain with no owner" )) else: if ticket['status'] is None: hints.append(tag_("The status will be '%(name)s'", name=status)) elif status != '*': hints.append( tag_("Next status will be '%(name)s'", name=status)) return (this_action['label'], tag(separated(control, ' ')), tag(separated(hints, '. ', '.') if hints else ''))
class SetOwnerToSelfAttributeTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True) self.ctlr = TicketSystem(self.env).action_controllers[0] self.req = MockRequest(self.env, authname='user1') ps = PermissionSystem(self.env) for user in ('user1', 'user2'): ps.grant_permission(user, 'TICKET_MODIFY') self.env.insert_users([('user1', 'User 1', None), ('user2', 'User 2', None)]) def _get_ticket_actions(self, req, ticket): return [ action[1] for action in self.ctlr.get_ticket_actions(req, ticket) ] def _reload_workflow(self): self.ctlr.actions = self.ctlr.get_all_actions() def _insert_ticket(self, status, owner, resolution=None): ticket = Ticket(self.env) ticket['status'] = status ticket['owner'] = owner if resolution: ticket['resolution'] = resolution ticket.insert() return ticket def test_owner_is_other(self): """Ticket owner is not auth'ed user. The workflow action is shown when the state will be changed by the action. """ ticket = self._insert_ticket('accepted', 'user2') args = self.req, ticket, 'accept' label, control, hints = self.ctlr.render_ticket_action_control(*args) ticket_actions = self._get_ticket_actions(*args[0:2]) self.assertIn('accept', ticket_actions) self.assertEqual(label, 'accept') self.assertEqual('', unicode(control)) self.assertEqual( 'The owner will be changed from ' '<span class="trac-author">User 2</span> to ' '<span class="trac-author-user">User 1</span>.', unicode(hints)) def test_owner_is_self_and_state_change(self): """Ticket owner is auth'ed user with state change. The workflow action is shown when the state will be changed by the action, even when the ticket owner is the authenticated user. """ ticket = self._insert_ticket('new', 'user1') args = self.req, ticket, 'accept' label, control, hints = self.ctlr.render_ticket_action_control(*args) ticket_actions = self._get_ticket_actions(*args[0:2]) self.assertIn('accept', ticket_actions) self.assertEqual(label, 'accept') self.assertEqual('', unicode(control)) self.assertEqual( 'The owner will remain <span class="trac-author-user">' 'User 1</span>.', unicode(hints)) def test_owner_is_self_and_no_state_change(self): """Ticket owner is the auth'ed user and no state change. The ticket action is not in the list of available actions when the state will not be changed by the action and the ticket owner is the authenticated user. """ ticket = self._insert_ticket('accepted', 'user1') args = self.req, ticket, 'accept' ticket_actions = self._get_ticket_actions(*args[0:2]) self.assertNotIn('accept', ticket_actions) def test_owner_is_self_state_change_and_multiple_operations(self): """Ticket owner is auth'ed user, state change and multiple ops. The set_owner_to_self workflow hint is shown when the ticket status is changed by the action, even when the ticket owner is the authenticated user. """ ticket = self._insert_ticket('new', 'user1') workflow = self.env.config['ticket-workflow'] workflow.set('resolve_as_owner', '* -> closed') workflow.set('resolve_as_owner.operations', 'set_owner_to_self, set_resolution') workflow.set('resolve_as_owner.set_resolution', 'fixed') self._reload_workflow() args = self.req, ticket, 'resolve_as_owner' label, control, hints = self.ctlr.render_ticket_action_control(*args) ticket_actions = self._get_ticket_actions(*args[0:2]) self.assertIn('resolve_as_owner', ticket_actions) self.assertEqual(label, 'resolve as owner') self.assertEqual( 'as fixed<input id="action_resolve_as_owner_resolve_resolution" ' 'name="action_resolve_as_owner_resolve_resolution" type="hidden" ' 'value="fixed" />', unicode(control)) self.assertEqual( 'The owner will remain <span class="trac-author-user">User 1' '</span>. The resolution will be set to fixed.', unicode(hints)) def test_owner_is_self_no_state_change_and_multiple_operations(self): """Ticket owner is auth'ed user, no state change and multiple ops. The set_owner_to_self workflow hint is not shown when the ticket state is not changed by the action and the ticket owner is the authenticated user. """ ticket = self._insert_ticket('closed', 'user1', 'fixed') workflow = self.env.config['ticket-workflow'] workflow.set('fix_resolution', 'closed -> closed') workflow.set('fix_resolution.operations', 'set_owner_to_self, set_resolution') workflow.set('fix_resolution.set_resolution', 'invalid') self._reload_workflow() args = self.req, ticket, 'fix_resolution' label, control, hints = self.ctlr.render_ticket_action_control(*args) ticket_actions = self._get_ticket_actions(*args[0:2]) self.assertIn('fix_resolution', ticket_actions) self.assertEqual(label, 'fix resolution') self.assertEqual( 'as invalid<input id="action_fix_resolution_resolve_resolution" ' 'name="action_fix_resolution_resolve_resolution" type="hidden" ' 'value="invalid" />', unicode(control)) self.assertEqual('The resolution will be set to invalid.', unicode(hints))
def get_ticket_group_stats(self, ticket_ids): total_cnt = len(ticket_ids) all_statuses = set(TicketSystem(self.env).get_all_status()) status_cnt = {} for s in all_statuses: status_cnt[s] = 0 if total_cnt: for status, count in self.env.db_query(""" SELECT status, count(status) FROM ticket WHERE id IN (%s) GROUP BY status """ % ",".join(str(x) for x in sorted(ticket_ids))): status_cnt[status] = count stat = TicketGroupStats(_('ticket status'), _('tickets')) remaining_statuses = set(all_statuses) groups = self._get_ticket_groups() catch_all_group = None # we need to go through the groups twice, so that the catch up group # doesn't need to be the last one in the sequence for group in groups: status_str = group['status'].strip() if status_str == '*': if catch_all_group: raise TracError(_( "'%(group1)s' and '%(group2)s' milestone groups " "both are declared to be \"catch-all\" groups. " "Please check your configuration.", group1=group['name'], group2=catch_all_group['name'])) catch_all_group = group else: group_statuses = set([s.strip() for s in status_str.split(',')]) \ & all_statuses if group_statuses - remaining_statuses: raise TracError(_( "'%(groupname)s' milestone group reused status " "'%(status)s' already taken by other groups. " "Please check your configuration.", groupname=group['name'], status=', '.join(group_statuses - remaining_statuses))) else: remaining_statuses -= group_statuses group['statuses'] = group_statuses if catch_all_group: catch_all_group['statuses'] = remaining_statuses for group in groups: group_cnt = 0 query_args = {} for s, cnt in status_cnt.iteritems(): if s in group['statuses']: group_cnt += cnt query_args.setdefault('status', []).append(s) for arg in [kv for kv in group.get('query_args', '').split(',') if '=' in kv]: k, v = [a.strip() for a in arg.split('=', 1)] query_args.setdefault(k, []).append(v) stat.add_interval(group.get('label', group['name']), group_cnt, query_args, group.get('css_class', group['name']), as_bool(group.get('overall_completion'))) stat.refresh_calcs() return stat