def test_agilo_properties(self): """Tests the TicketSystem for the agilo properties""" ats = AgiloTicketSystem(self.env) calc, allowed, sorting, showing = ats.get_agilo_properties(Type.TASK) # Tests are a bit hard but necessary, every time someone changes the # default properties this will fail, needs to be updated self.assert_equals({}, calc, "Found calculated properties for task: %s" % calc) self.assert_equals({}, allowed, "Found allowed link properties for task: %s" % allowed) self.assert_equals({}, sorting, "Found sorting properties for task: %s" % sorting) self.assert_equals({}, showing, "Found showing properties for task: %s" % showing) # Now tests a story calc, allowed, sorting, showing = ats.get_agilo_properties(Type.USER_STORY) # Tests are a bit hard but necessary, every time someone changes the # default properties this will fail, needs to be updated self.assert_true(Key.TOTAL_REMAINING_TIME in calc and Key.ESTIMATED_REMAINING_TIME in calc, "Wrong calculated properties for story: %s" % calc) self.assert_true(Type.TASK in allowed, "Wrong allowed link properties for story: %s" % allowed) self.assert_true(Type.TASK in sorting, "Wrong sorting properties for story: %s" % sorting) self.assert_true(Type.TASK in showing, "Wrong showing properties for story: %s" % showing)
def detail_view(self, req, cat, page, link): links_configuration = LinksConfiguration(self.env) (source, target) = links_configuration.extract_types(link) copy_fields = [f.strip() for f in self.links.get('%s.%s.%s' % \ (source, target, LinkOption.COPY), default='').split(',')] show_fields = [f.strip() for f in self.links.get('%s.%s.%s' % \ (source, target, LinkOption.SHOW), default='').split(',')] ticket_system = AgiloTicketSystem(self.env) # dict of name->label for all core and custom fields labels = dict([(f['name'], f['label']) for f in ticket_system.get_ticket_fields()]) cascade_delete = source + '-' + target in self._get_delete_pairs() data = { 'view': 'detail', 'link': link, 'source': source, 'target': target, 'source_fields': self.config.TYPES[source], 'target_fields': self.config.TYPES[target], 'labels': labels, 'copy_fields': copy_fields, 'show_fields': show_fields, 'cascade_delete': cascade_delete } return 'agilo_admin_links.html', data
def detail_view(self, req, cat, page, link): links_configuration = LinksConfiguration(self.env) (source, target) = links_configuration.extract_types(link) copy_fields = [f.strip() for f in self.links.get('%s.%s.%s' % \ (source, target, LinkOption.COPY), default='').split(',')] show_fields = [f.strip() for f in self.links.get('%s.%s.%s' % \ (source, target, LinkOption.SHOW), default='').split(',')] ticket_system = AgiloTicketSystem(self.env) # dict of name->label for all core and custom fields labels = dict([(f['name'], f['label']) for f in ticket_system.get_ticket_fields()]) cascade_delete = source+'-'+target in self._get_delete_pairs() data = { 'view': 'detail', 'link': link, 'source' : source, 'target' : target, 'source_fields' : self.config.TYPES[source], 'target_fields' : self.config.TYPES[target], 'labels' : labels, 'copy_fields' : copy_fields, 'show_fields' : show_fields, 'cascade_delete': cascade_delete } return 'agilo_admin_links.html', data
def save(self, db=None): """Override save to reset ticket fields in the ticket system""" result = super(Sprint, self).save(db=db) # Reset the fields for trac 0.11.2 from agilo.ticket.api import AgiloTicketSystem ats = AgiloTicketSystem(self.env) if hasattr(ats, 'reset_ticket_fields'): ats.reset_ticket_fields() return result
def save(self, db=None): """Override save to reset ticket fields in the ticket system""" result = super(Sprint, self).save(db=db) # Reset the fields for trac 0.11.2 from agilo.ticket.api import AgiloTicketSystem ats = AgiloTicketSystem(self.env) if hasattr(ats, "reset_ticket_fields"): ats.reset_ticket_fields() return result
def _hide_fields_not_configured_for_this_type(self, req, data): ticket_type = req.args[Key.TYPE] ats = AgiloTicketSystem(self.env) normalized_type = ats.normalize_type(ticket_type) data[Key.TYPE] = normalized_type fields_for_type = AgiloConfig(self.env).TYPES.get(normalized_type, []) create_perms = CoreTemplateProvider(self.env).create_permissions(req) if normalized_type in create_perms: for data_field in data['fields']: field_name = data_field[Key.NAME] if field_name == Key.TYPE: current_options = data_field.get('options', []) data_field['options'] = \ self._ticket_types_the_user_can_create_or_modify(normalized_type, current_options, create_perms) data['type_selection'] = data_field data_field[Key.SKIP] = True elif not field_name in fields_for_type: # Skip the field and the value if it's not for this type data_field[Key.SKIP] = True elif data_field[Key.SKIP] and (field_name not in MANDATORY_FIELDS): # We have to make all fields visible which belong to # this ticket type. Unfortunately, Trac just creates # a Ticket (though an AgiloTicket due to our monkey # patching) and we can't influence the instantiation # itself. # Therefore it just depends on the default ticket type # set in the environment what fields this ticket has. # Therefore some fields are marked skipped although they # should be displayed. # trac itself skips some fields because it want to have # more control over the positioning. We have to respect # that. # fs, 04.11.2008: I thought about moving this condition # to_prepare_fields where I think this code should live # but then I would have to copy all the conditions and # probably the code is here for a good reason so I'm # just adding it here. data_field[Key.SKIP] = False elif field_name == Key.OWNER and ats.restrict_owner: # we need to transform the field into a list of users ats.eventually_restrict_owner(data_field) elif len(create_perms) > 0: # Redirect to the first allowed type for the given user. first_allowed_type = create_perms[0] req.redirect(req.href.newticket(type=first_allowed_type)) else: if ticket_type not in AgiloConfig(self.env).ALIASES: raise TracError(u"Unkown type '%s'!" % ticket_type) raise TracError("You are not allowed to create a %s!" % ticket_type) return data
def get_custom_fields(self, field_name=None): """ Returns the custom fields from TicketSystem component. Use a field name to find a specific custom field only """ if not field_name: # return full list return AgiloTicketSystem(self.env).get_custom_fields() else: # only return specific item with cfname all = AgiloTicketSystem(self.env).get_custom_fields() for item in all: if item[Key.NAME] == field_name: return item return None # item not found
def _validate_ticket(self, req, ticket, force_collision_check=False): if 'ts' in req.args: match = re.search('^(\d+)$', req.args.get('ts') or '') if match: timestamp = int(match.group(1)) last_changed = datetime.utcfromtimestamp(timestamp).replace(tzinfo=utc) req.args['ts'] = str(last_changed) ticket_system = AgiloTicketSystem(self.env) trac_ticket_system = super(AgiloTicketModule, self) if ticket_system.is_trac_012(): return trac_ticket_system._validate_ticket(req, ticket, force_collision_check=force_collision_check) return trac_ticket_system._validate_ticket(req, ticket)
def detail_save_view(self, req, cat, page, ticket_type): """Save the detail panel view""" # The types will be stored in lowercase and the space is not a # valid character for the config file key ticket_type = normalize_ticket_type(ticket_type) alias = req.args.get(Key.ALIAS) if alias: self.agilo_config.change_option('%s.%s' % (ticket_type, Key.ALIAS), alias, section=AgiloConfig.AGILO_TYPES, save=False) # save fields as string or comma-separated list of values # FIXME: (AT) after going crazy, it came out that some types are not # saved because there is no specific field assigned and the config # won't store the property in the trac.ini. So the agilo type will also # not be loaded, even if the alias would be set. fields = req.args.get(Key.FIELDS, '') # We have to save strings not lists if isinstance(fields, list): fields = ', '.join(fields) self.agilo_config.change_option(ticket_type, fields, section=AgiloConfig.AGILO_TYPES, save=False) calc = [] for res, func in zip(req.args.getlist('result'), req.args.getlist('function')): if res and func: configstring = u'%s=%s' % (res.strip(), func.strip()) parsed = parse_calculated_field(configstring) if parsed == None: msg = u"Wrong format for calculated property '%s'" raise TracError(_(msg) % res) calc.append(configstring) calc = ','.join(calc) if calc: self.agilo_config.change_option( '%s.%s' % (ticket_type, LinkOption.CALCULATE), calc, section=AgiloConfig.AGILO_LINKS, save=False) self.agilo_config.save() # on 0.12 we need to reset the ticket fields explicitely as the synchronization # is not done with the trac.ini anymore if AgiloTicketSystem(self.env).is_trac_012(): AgiloTicketSystem(self.env).reset_ticket_fields() return req.redirect(req.href.admin(cat, page))
def _validate_ticket(self, req, ticket, force_collision_check=False): if 'ts' in req.args: match = re.search('^(\d+)$', req.args.get('ts') or '') if match: timestamp = int(match.group(1)) last_changed = datetime.utcfromtimestamp(timestamp).replace( tzinfo=utc) req.args['ts'] = str(last_changed) ticket_system = AgiloTicketSystem(self.env) trac_ticket_system = super(AgiloTicketModule, self) if ticket_system.is_trac_012(): return trac_ticket_system._validate_ticket( req, ticket, force_collision_check=force_collision_check) return trac_ticket_system._validate_ticket(req, ticket)
def test_non_existent_type(self): """Tests that if a type is not defined as agilo type will get all properties""" nonex = self.teh.create_ticket('nonex') all_fields = AgiloTicketSystem(self.env).get_ticket_fields() for f1, f2 in zip(nonex.fields, all_fields): self.assert_equals(f1, f2, "Error: %s != %s" % (f1, f2))
def update_custom_field(self, customfield, create=False): """ Update or create a new custom field (if requested). customfield is a dictionary with the following possible keys: name = name of field (alphanumeric only) type = text|checkbox|select|radio|textarea label = label description value = default value for field content options = options for select and radio types (list, leave first empty for optional) cols = number of columns for text area rows = number of rows for text area order = specify sort order for field """ self._validate_input(customfield, create) f_type = customfield[Key.TYPE] if f_type == 'textarea': def set_default_value(key, default): if (key not in customfield) or \ (not unicode(customfield[key]).isdigit()): customfield[key] = unicode(default) # dwt: why is this called twice? set_default_value(Key.COLS, 60) set_default_value(Key.COLS, 5) if create: number_of_custom_fields = len(self.get_custom_fields()) # We assume that the currently added custom field is not present in # the return value of get_custom_fields and we start counting from 0 customfield[Key.ORDER] = str(number_of_custom_fields) self._store_all_options_for_custom_field(customfield) AgiloTicketSystem(self.env).reset_ticket_fields()
def test_can_serialize_task_to_dict(self): task = AgiloTicket(self.env, t_type=Type.TASK) self.assertNotEqual('fixed', task[Key.RESOLUTION]) task[Key.SUMMARY] = 'My Summary' task.insert() expected = { # required Key.ID: task.id, Key.TYPE: Type.TASK, Key.SUMMARY: 'My Summary', Key.DESCRIPTION: '', Key.STATUS: '', Key.RESOLUTION: '', Key.REPORTER: '', Key.OWNER: '', # type specific Key.SPRINT: '', Key.REMAINING_TIME: '', Key.RESOURCES: '', # Key.Options is not used in order to reduce required data to # transfer for a backlog load. 'outgoing_links': [], 'incoming_links': [], 'time_of_last_change': to_timestamp(task.time_changed), 'ts': str(task.time_changed), } if AgiloTicketSystem.is_trac_1_0(): from trac.util.datefmt import to_utimestamp expected.update({'view_time': str(to_utimestamp(task.time_changed))}) self.assert_equals(expected, task.as_dict())
def test_can_serialize_task_to_dict(self): task = AgiloTicket(self.env, t_type=Type.TASK) self.assertNotEqual('fixed', task[Key.RESOLUTION]) task[Key.SUMMARY] = 'My Summary' task.insert() expected = { # required Key.ID: task.id, Key.TYPE: Type.TASK, Key.SUMMARY: 'My Summary', Key.DESCRIPTION: '', Key.STATUS: '', Key.RESOLUTION: '', Key.REPORTER: '', Key.OWNER: '', # type specific Key.SPRINT: '', Key.REMAINING_TIME: '', Key.RESOURCES: '', # Key.Options is not used in order to reduce required data to # transfer for a backlog load. 'outgoing_links': [], 'incoming_links': [], 'time_of_last_change': to_timestamp(task.time_changed), 'ts': str(task.time_changed), } if AgiloTicketSystem.is_trac_1_0(): from trac.util.datefmt import to_utimestamp expected.update( {'view_time': str(to_utimestamp(task.time_changed))}) self.assert_equals(expected, task.as_dict())
def setUp(self): self.super() self.env.config.set('components', 'agilo.scrum.backlog.model.backlogupdater', 'disabled') self.env.config.set('components', 'agilo.scrum.burndown.changelistener.burndowndatachangelistener', 'disabled') # so the changes persist after the shutdown self.env.config.save() if not AgiloTicketSystem(self.env).is_trac_012(): self.env.shutdown() # restart: so the listeneres that where registered before this test are actually disabled self.env = self.testenv.get_trac_environment() self.task = self.teh.create_task(sprint=self.sprint_name())
def test_fields_for_task(self): """Tests how the FieldsWrapper respond with a task type""" ticket_system = AgiloTicketSystem(self.env) tfw = FieldsWrapper(self.env, ticket_system.get_ticket_fields(), Type.TASK) expected_fields_for_task = AgiloConfig(self.env).TYPES.get(Type.TASK) # Try to check the keys field_wrapper_names = map(lambda field: field[Key.NAME], tfw) # it is added by the ticket system to store sprint # scope synchronous with milestone field_wrapper_names.remove(Key.MILESTONE) expected_fields_for_task.remove('id') if not ticket_system.is_trac_012(): # in Trac 0.11, time fields are magic field_wrapper_names.append('changetime') field_wrapper_names.append('time') self.assert_equals(sorted(expected_fields_for_task), sorted(field_wrapper_names))
def test_safe_custom_fields(self): """Tests that the defined custom fields are safe""" ticket_custom = \ AgiloConfig(self.env).get_section(AgiloConfig.TICKET_CUSTOM) custom_fields = ticket_custom.get_options_matching_re('^[^.]+$') tfw = FieldsWrapper(self.env, AgiloTicketSystem(self.env).get_ticket_fields(), Type.TASK) for f in tfw: if f[Key.NAME] in custom_fields: self.assert_true(f[Key.CUSTOM], "%s should be custom!!!" % f[Key.NAME])
def interesting_fieldnames(self): fieldnames = [] for field in AgiloTicketSystem(self.env).get_ticket_fields(): fieldname = field[Key.NAME] # AT: here key must be a string or will break the call # afterwards try: fieldname = str(fieldname) except ValueError, e: warning(self, "Fieldname: %s is not a string... %s" % \ (repr(fieldname), exception_to_unicode(e))) continue fieldnames.append(fieldname)
def runTest(self): """Tests the ticket delete method""" self._tester.login_as(Usernames.admin) ticket_id = self._tester.create_new_agilo_userstory('Delete Me') self._tester.go_to_view_ticket_page(ticket_id) tc.formvalue('propertyform', 'delete', 'click') tc.submit('delete') self._tester.go_to_view_ticket_page(ticket_id, should_fail=True) tc.code(404) from agilo.ticket import AgiloTicketSystem if AgiloTicketSystem.is_trac_1_0(): return else: self._tester._set_ticket_id_sequence(new_value=self._tester.ticketcount)
def runTest(self): """Tests the ticket delete method""" self._tester.login_as(Usernames.admin) ticket_id = self._tester.create_new_agilo_userstory('Delete Me') self._tester.go_to_view_ticket_page(ticket_id) tc.formvalue('propertyform', 'delete', 'click') tc.submit('delete') self._tester.go_to_view_ticket_page(ticket_id, should_fail=True) tc.code(404) from agilo.ticket import AgiloTicketSystem if AgiloTicketSystem.is_trac_1_0(): return else: self._tester._set_ticket_id_sequence( new_value=self._tester.ticketcount)
def delete_custom_field(self, field_name): """Deletes a custom field""" if not self.ticket_custom.get(field_name): return # Nothing to do here - cannot find field # Need to redo the order of fields that are after the field to be deleted order_to_delete = self.ticket_custom.get_int('%s.%s' % (field_name, Key.ORDER)) cfs = self.get_custom_fields() for field in cfs: if field[Key.ORDER] > order_to_delete: field[Key.ORDER] -= 1 self._set_custom_field_value(field, Key.ORDER) elif field[Key.NAME] == field_name: # Remove any data for the custom field (covering all bases) self._del_custom_field_value(field) # Save settings self.ticket_custom.save() AgiloTicketSystem(self.env).reset_ticket_fields()
def runTest(self): # Setting the a default type different from the ticket type to be # created triggered another bug in the preview display... self._tester.login_as(Usernames.admin) self._tester.login_as(Usernames.product_owner) title = 'Foo Bar Title' ticket_id = self._tester.create_new_agilo_userstory(title) self._tester.go_to_view_ticket_page(ticket_id) new_title = 'bulb' tc.formvalue('propertyform', 'summary', new_title) tc.submit('preview') tc.code(200) from agilo.ticket.api import AgiloTicketSystem if AgiloTicketSystem.is_trac_1_0(): tc.find('<span[^>]*>%s</span>' % new_title) else: tc.find('<h2[^>]*>%s</h2>' % new_title)
def _create_environment(self, env_dir, db_url, repo_type, repodir): if self._running_on_windows(): # On Windows we need to double-escape backslashes - otherwise they # will be used as escape characters when storing the config (repo # dir in trac.ini won't contain any backslashes then). repodir = repodir.replace('\\', '\\\\') self._create_database(db_url) self._tracadmin('initenv', env_dir, db_url, repo_type, repodir) env = self.get_trac_environment() config = env.config for component in self.get_enabled_components(): config.set('components', component, 'enabled') for component in self.get_disabled_components(): config.set('components', component, 'disabled') for (section, name, value) in self.get_config_options(): config.set(section, name, value) config.save() config.touch() if not AgiloTicketSystem(env).is_trac_012(): env.shutdown()
def test_owner_team_member_with_sprint_related_tickets(self): """ Tests that the owner of a sprint related ticket, in case of restrict_owner option, is limited to the team members assigned to the sprint, in case the ticket has the Remaining Time property """ teh = self.teh env = self.env self.assert_true(AgiloTicketSystem(env).restrict_owner) t = teh.create_team('A-Team') self.assert_true(t.save()) s = teh.create_sprint('Test Me', team=t) self.assert_equals('Test Me', s.name) self.assert_equals(s.team, t) tm1 = teh.create_member('TM1', t) self.assert_true(tm1.save()) tm2 = teh.create_member('TM2', t) self.assert_true(tm2.save()) tm3 = teh.create_member('TM3') self.assert_true(tm3.save()) self.assert_contains(tm1.name, [m.name for m in t.members]) self.assert_contains(tm2.name, [m.name for m in t.members]) # Now make a ticket that is dependent from the sprint task = teh.create_ticket(Type.TASK, props={ Key.REMAINING_TIME: '12.5', Key.OWNER: tm1.name, Key.SPRINT: s.name }) self.assert_equals(s.name, task[Key.SPRINT]) f = task.get_field(Key.OWNER) self.assert_true(tm1.name in f[Key.OPTIONS]) self.assert_true(tm2.name in f[Key.OPTIONS]) self.assert_false(tm3.name in f[Key.OPTIONS])
def interesting_fieldnames(self): fieldnames = [Key.ID, 'ticket'] for field in AgiloTicketSystem(self.env).get_ticket_fields(): fieldname = field[Key.NAME] fieldnames.append(fieldname) return fieldnames
def _bug_calculated_properties(self): return AgiloTicketSystem(self.env).get_agilo_properties('bug')[0].keys()
def test_can_map_alias_to_trac_type(self): ticket_system = AgiloTicketSystem(self.env) self.assert_equals(Type.TASK, ticket_system.normalize_type(Type.TASK)) self.assert_equals(Type.TASK, ticket_system.normalize_type('Task')) #Task alias
def tearDown(self): if not AgiloTicketSystem(self.env).is_trac_012(): self.env.shutdown() self.testenv.remove() self.super()
def _previous_type_had_field(self, fieldname, old_values): previous_type = old_values[Key.TYPE] ticket_system = AgiloTicketSystem(self.env) return fieldname in ticket_system.get_ticket_fieldnames(previous_type)
def test_knows_which_fieldnames_are_valid_for_a_ticket_type(self): ticket_system = AgiloTicketSystem(self.env) self.assert_contains(Key.REMAINING_TIME, ticket_system.get_ticket_fieldnames(Type.TASK)) self.assert_not_contains(Key.BUSINESS_VALUE, ticket_system.get_ticket_fieldnames(Type.TASK))
def test_just_return_input_if_no_alias_was_found(self): ticket_system = AgiloTicketSystem(self.env) self.assert_none(ticket_system.normalize_type('nonexisting_type'))