Example #1
0
    def test_doesnt_overwrite_custom_favicons(self):
        custom_favicon = 'fnord'
        project_section = AgiloConfig(self.env).get_section('project')
        project_section.change_option('icon', custom_favicon)
        self.assert_equals(custom_favicon, project_section.get('icon'))

        initialize_config(self.env, {})
        project_section = AgiloConfig(self.env).get_section('project')
        self.assert_equals(custom_favicon, project_section.get('icon'))
Example #2
0
 def test_doesnt_overwrite_custom_favicons(self):
     custom_favicon = 'fnord'
     project_section = AgiloConfig(self.env).get_section('project')
     project_section.change_option('icon', custom_favicon)
     self.assert_equals(custom_favicon, project_section.get('icon'))
     
     initialize_config(self.env, {})
     project_section = AgiloConfig(self.env).get_section('project')
     self.assert_equals(custom_favicon, project_section.get('icon'))
Example #3
0
class InstallationTest(AgiloTestCase):
    """Tests agilo installation process, calling the main init method"""
    def setUp(self):
        self.super()
        self.config = AgiloConfig(self.env)

    def test_initialization_adds_agilo_policy(self):
        self.assert_equals(
            self.config.get('permission_policies', section='trac'),
            u'AgiloPolicy, DefaultPermissionPolicy, LegacyAttachmentPolicy')

    def test_initialization_set_templates_dir(self):
        template_path = self.config.get('templates_dir', section='inherit')
        self.assert_true(template_path.find('templates') > -1)
Example #4
0
class InstallationTest(AgiloTestCase):
    """Tests agilo installation process, calling the main init method"""
    
    def setUp(self):
        self.super()
        self.config = AgiloConfig(self.env)
    
    def test_initialization_adds_agilo_policy(self):
        self.assert_equals(self.config.get('permission_policies', section='trac'),
                          u'AgiloPolicy, DefaultPermissionPolicy, LegacyAttachmentPolicy')
    
    def test_initialization_set_templates_dir(self):
        template_path = self.config.get('templates_dir', section='inherit')
        self.assert_true(template_path.find('templates') > -1)
 def setUp(self):
     self.super()
     config = AgiloConfig(self.env)
     # Adding properties for multiple calculated property testing
     # The problem is that at this point the linkConfiguration as been
     # already initialized so we will need to do it again manually
     config.change_option('actual_time', 'text', 
                          section=AgiloConfig.TICKET_CUSTOM)
     config.change_option(Type.TASK, 
                          'sprint, remaining_time, actual_time, estimated_time, owner, drp_resources',
                          section=AgiloConfig.AGILO_TYPES)
     config.change_option('story.calculate', 
                          'total_remaining_time=sum:get_outgoing.remaining_time, total_actual_time=sum:get_outgoing.actual_time',
                          section=AgiloConfig.AGILO_LINKS)
     config.save()
     
     self.assert_true(config.is_agilo_enabled)
     self.assert_true('actual_time' in config.get_list(Type.TASK, section=AgiloConfig.AGILO_TYPES))
     self.assert_true('actual_time' in config.get_fields_for_type().get(Type.TASK))
     self.assert_equals(config.get('story.calculate', section=AgiloConfig.AGILO_LINKS), 
                      'total_remaining_time=sum:get_outgoing.remaining_time, total_actual_time=sum:get_outgoing.actual_time')
     self._reset_links_configuration()
     # Creates tickets
     self.t1 = self.teh.create_ticket(Type.USER_STORY)
     self.t2 = self.teh.create_ticket(Type.TASK, 
                                      props={Key.REMAINING_TIME: u'20', 
                                             Key.RESOURCES: u'Tim, Tom'})
     self.t3 = self.teh.create_ticket(Type.TASK, 
                                      props={Key.REMAINING_TIME: u'10', 
                                             Key.RESOURCES: u'Tim, Tom'})
     # Now actual_time should be a valid field for Task...
     self.assert_not_none(self.t2.get_field('actual_time'))
Example #6
0
 def _set_default_type(self, new_value):
     agilo_config = AgiloConfig(self.env)
     old_value = agilo_config.get('default_type', 'ticket')
     agilo_config.change_option('default_type',
                                new_value,
                                section='ticket',
                                save=True)
     return old_value
 def runTest(self):
     self._tester.login_as(Usernames.admin)
     fieldname = 'remaining_time'
     self._tester.modify_custom_field(fieldname, value='5')
     self._tester.modify_custom_field(fieldname, value='')
     
     url = '%s/admin/agilo/fields/%s' % (self._tester.url, fieldname)
     tc.go(url)
     tc.find('<input type="text" name="value" value=""')
     
     env = self._testenv.get_trac_environment()
     config = AgiloConfig(env).get_section(AgiloConfig.TICKET_CUSTOM)
     self.assertEqual(None, config.get('remaining_time.value'))
 def populate_with_sprint_data(self, sprint_name):
     """Populate the chart with sprint statistics"""
     cmd_stats = SprintController.GetTicketsStatisticsCommand(self.env,
                                                              sprint=sprint_name)
     tickets_stats = SprintController(self.env).process_command(cmd_stats)
     
     planned, closed, total, labels = [], [], [], []
     
     aliases = AgiloConfig(self.env).ALIASES
     for i, t_type in enumerate(tickets_stats):
         nr_planned, nr_in_progress, nr_closed = tickets_stats[t_type]
         planned.append((i, nr_planned))
         closed.append((i, nr_closed))
         nr_total = nr_planned + nr_in_progress + nr_closed
         total.append((i, nr_total))
         alias = aliases.get(t_type, t_type)
         labels.append((i, alias))
     
     self.data.update(
         dict(sprint_name=sprint_name, labels=labels, 
              planned=planned, closed=closed, total=total)
     )
Example #9
0
    def populate_with_sprint_data(self, sprint_name):
        """Populate the chart with sprint statistics"""
        cmd_stats = SprintController.GetTicketsStatisticsCommand(
            self.env, sprint=sprint_name)
        tickets_stats = SprintController(self.env).process_command(cmd_stats)

        planned, closed, total, labels = [], [], [], []

        aliases = AgiloConfig(self.env).ALIASES
        for i, t_type in enumerate(tickets_stats):
            nr_planned, nr_in_progress, nr_closed = tickets_stats[t_type]
            planned.append((i, nr_planned))
            closed.append((i, nr_closed))
            nr_total = nr_planned + nr_in_progress + nr_closed
            total.append((i, nr_total))
            alias = aliases.get(t_type, t_type)
            labels.append((i, alias))

        self.data.update(
            dict(sprint_name=sprint_name,
                 labels=labels,
                 planned=planned,
                 closed=closed,
                 total=total))
Example #10
0
class ChartGenerator(Component):
    """The ChartGenerator can be asked to generate a widget for one of the 
    implemented charts. Furthermore it can decide to cache charts."""

    widget_generators = ExtensionPoint(IAgiloWidgetGenerator)

    def __init__(self):
        self.cache = {}
        self.config = AgiloConfig(self.env).get_section(AGILO_CHARTS)

    def _get_generator(self, name):
        generator = None
        for widget_generator in self.widget_generators:
            if widget_generator.can_generate_widget(name):
                generator = widget_generator
                break
        if generator == None:
            raise TracError(u'Unknown widget type %s' % name)
        return generator

    def _get_cache_key(self, generator, name, values):
        data = dict(name=name, **values)
        if hasattr(generator, 'get_cache_components'):
            cache_components = generator.get_cache_components(data.keys())
        else:
            cache_components = ('name', 'sprint_name')

        cache_values = []
        for item in cache_components:
            if item not in data:
                return None
            cache_values.append((item, data[item]))
        # We need a hashable (non-mutable) type here therefore a dict won't
        # work. I did not want to resort to some url-like encoding because then
        # it is not 100% clear how to identify the separate items afterwards.
        # So a tuple of tuples (key, value) should work ok.
        cache_key = tuple(cache_values)
        return cache_key

    # REFACT: get rid of this configurability - it's not used and not supported on newer versions
    def _add_configured_colors(self, kwargs):
        if 'FORE_COLOR' not in kwargs:
            # The config key here is chosen because it was used in the constant
            # Option.FORE_COLOR until 0.7.3 and I did not want to break
            # backwards compatibility without good reason (fs, 2009-01-23)
            color = self.config.get('sprintstats.foreground',
                                    default='#4180b3')
            kwargs['FORE_COLOR'] = color
        if 'BACK_COLOR' not in kwargs:
            color = self.config.get('sprintstats.background',
                                    default='#94d31a')
            kwargs['BACK_COLOR'] = color

    def _get_widget(self, name, kwargs, use_cache):
        # The idea behind this caching is that getting the widget from the
        # generator is by far the most expensive operation (because it will
        # populate the widget with all necessary data). On the other hand,
        # creating the real chart is cheap (this is especially true for charts
        # generated by flot but holds true also for matplotlib).
        # Furthermore I assume that making a copy of dicts with some (<20) keys
        # is probably also cheap. The copy frees us from any thoughts about
        # threading and multiple instances.
        new_widget = None

        generator = self._get_generator(name)
        cache_key = None
        if use_cache:
            cache_key = self._get_cache_key(generator, name, kwargs)
        if cache_key is not None and cache_key in self.cache:
            cached_widget = self.cache[cache_key]
            new_widget = cached_widget.copy()
        else:
            self._add_configured_colors(kwargs)
            new_widget = generator.generate_widget(name, **kwargs)
            if cache_key is not None:
                # Maybe clean cache after a certain amount of time/certain size?
                self.cache[cache_key] = new_widget.copy()
            kwargs = {
            }  # don't overwrite everything again with .update() below
        return new_widget

    def _set_widget_dimensions(self, widget, name, kwargs):
        """Sets the dimensions if they were specified by the user. If 
        width/height were in kwargs, these will be deleted."""
        width, height = kwargs.get(Key.WIDTH), kwargs.get(Key.HEIGHT)
        if width == None or height == None:
            config_width = self.config.get_int('%s.%s' % (name, Key.WIDTH))
            config_height = self.config.get_int('%s.%s' % (name, Key.HEIGHT))
            if width != None and height != None:
                (width, height) = (config_width, config_height)
        if width is not None and height is not None:
            widget.set_dimensions(width, height)
        # do not set these using update_data
        kwargs.pop(Key.HEIGHT, None)
        kwargs.pop(Key.WIDTH, None)

    def get_chartwidget(self, name, use_cache=True, **kwargs):
        """Return a widget instance which will generate the chart's HTML."""

        new_widget = self._get_widget(name, kwargs, use_cache)
        self._set_widget_dimensions(new_widget, name, kwargs)
        new_widget.update_data(**kwargs)
        return new_widget

    def _add_names_for_persistent_objects(self, changed_items):
        """We don't want to care if a user passes a PersistentObject
        instance or its name so this method just grabs a name if it is
        available and adds it the changed_items (this is no problem
        because as long as *one* cache component is in the cache key,
        the cache item will be invalidated."""
        new_items = {}
        for key in changed_items:
            value = changed_items[key]
            if hasattr(value, 'name'):
                new_key = key + '_name'
                new_items[new_key] = value.name
        new_items.update(changed_items)
        return new_items

    def invalidate_cache(self, **changed_items):
        changed_items = self._add_names_for_persistent_objects(changed_items)
        if len(changed_items) == 0:
            self.cache = {}
        else:
            del_keys = []
            for cache_key in self.cache:
                for key, value in cache_key:
                    if (key in changed_items) and (value
                                                   == changed_items[key]):
                        del_keys.append(cache_key)
                        break
            for key in del_keys:
                del self.cache[key]
Example #11
0
class ChartGenerator(Component):
    """The ChartGenerator can be asked to generate a widget for one of the 
    implemented charts. Furthermore it can decide to cache charts."""
    
    widget_generators = ExtensionPoint(IAgiloWidgetGenerator)
    
    def __init__(self):
        self.cache = {}
        self.config = AgiloConfig(self.env).get_section(AGILO_CHARTS)
    
    def _get_generator(self, name):
        generator = None
        for widget_generator in self.widget_generators:
            if widget_generator.can_generate_widget(name):
                generator = widget_generator
                break
        if generator == None:
            raise TracError(u'Unknown widget type %s' % name)
        return generator
    
    def _get_cache_key(self, generator, name, values):
        data = dict(name=name, **values)
        if hasattr(generator, 'get_cache_components'):
            cache_components = generator.get_cache_components(data.keys())
        else:
            cache_components = ('name', 'sprint_name')
        
        cache_values = []
        for item in cache_components:
            if item not in data:
                return None
            cache_values.append((item, data[item]))
        # We need a hashable (non-mutable) type here therefore a dict won't 
        # work. I did not want to resort to some url-like encoding because then
        # it is not 100% clear how to identify the separate items afterwards.
        # So a tuple of tuples (key, value) should work ok.
        cache_key = tuple(cache_values)
        return cache_key
    
    # REFACT: get rid of this configurability - it's not used and not supported on newer versions
    def _add_configured_colors(self, kwargs):
        if 'FORE_COLOR' not in kwargs:
            # The config key here is chosen because it was used in the constant
            # Option.FORE_COLOR until 0.7.3 and I did not want to break 
            # backwards compatibility without good reason (fs, 2009-01-23)
            color = self.config.get('sprintstats.foreground', 
                                    default='#4180b3')
            kwargs['FORE_COLOR'] = color
        if 'BACK_COLOR' not in kwargs:
            color = self.config.get('sprintstats.background', 
                                    default='#94d31a')
            kwargs['BACK_COLOR'] = color
    
    def _get_widget(self, name, kwargs, use_cache):
        # The idea behind this caching is that getting the widget from the 
        # generator is by far the most expensive operation (because it will 
        # populate the widget with all necessary data). On the other hand, 
        # creating the real chart is cheap (this is especially true for charts
        # generated by flot but holds true also for matplotlib).
        # Furthermore I assume that making a copy of dicts with some (<20) keys
        # is probably also cheap. The copy frees us from any thoughts about
        # threading and multiple instances.
        new_widget = None
        
        generator = self._get_generator(name)
        cache_key = None
        if use_cache:
            cache_key = self._get_cache_key(generator, name, kwargs)
        if cache_key is not None and cache_key in self.cache:
            cached_widget = self.cache[cache_key]
            new_widget = cached_widget.copy()
        else:
            self._add_configured_colors(kwargs)
            new_widget = generator.generate_widget(name, **kwargs)
            if cache_key is not None:
                # Maybe clean cache after a certain amount of time/certain size?
                self.cache[cache_key] = new_widget.copy()
            kwargs = {} # don't overwrite everything again with .update() below
        return new_widget
    
    def _set_widget_dimensions(self, widget, name, kwargs):
        """Sets the dimensions if they were specified by the user. If 
        width/height were in kwargs, these will be deleted."""
        width, height = kwargs.get(Key.WIDTH), kwargs.get(Key.HEIGHT)
        if width == None or height == None:
            config_width = self.config.get_int('%s.%s' % (name, Key.WIDTH))
            config_height = self.config.get_int('%s.%s' % (name, Key.HEIGHT))
            if width != None and height != None:
                (width, height) = (config_width, config_height)
        if width is not None and height is not None:
            widget.set_dimensions(width, height)
        # do not set these using update_data
        kwargs.pop(Key.HEIGHT, None)
        kwargs.pop(Key.WIDTH, None)
    
    def get_chartwidget(self, name, use_cache=True, **kwargs):
        """Return a widget instance which will generate the chart's HTML."""
        
        new_widget  = self._get_widget(name, kwargs, use_cache)
        self._set_widget_dimensions(new_widget, name, kwargs)
        new_widget.update_data(**kwargs)
        return new_widget
    
    def _add_names_for_persistent_objects(self, changed_items):
        """We don't want to care if a user passes a PersistentObject
        instance or its name so this method just grabs a name if it is
        available and adds it the changed_items (this is no problem
        because as long as *one* cache component is in the cache key,
        the cache item will be invalidated."""
        new_items = {}
        for key in changed_items:
            value = changed_items[key]
            if hasattr(value, 'name'):
                new_key = key + '_name'
                new_items[new_key] = value.name
        new_items.update(changed_items)
        return new_items
    
    def invalidate_cache(self, **changed_items):
        changed_items = self._add_names_for_persistent_objects(changed_items)
        if len(changed_items) == 0:
            self.cache = {}
        else:
            del_keys = []
            for cache_key in self.cache:
                for key, value in cache_key:
                    if (key in changed_items) and (value == changed_items[key]):
                        del_keys.append(cache_key)
                        break
            for key in del_keys:
                del self.cache[key]
Example #12
0
def set_config_value(env, name, section, new_value):
    agilo_config = AgiloConfig(env)
    old_value = agilo_config.get(name, section)
    agilo_config.change_option(name, new_value, section=section, save=True)
    return old_value
Example #13
0
 def _set_default_type(self, new_value):
     agilo_config = AgiloConfig(self.env)
     old_value = agilo_config.get('default_type', 'ticket')
     agilo_config.change_option('default_type', new_value, 
                                section='ticket', save=True)
     return old_value
Example #14
0
class AgiloConfigTest(AgiloTestCase):
    
    def setUp(self):
        self.super()
        self.trac_config = self.env.config
        self.config = AgiloConfig(self.env)
    
    # --------------------------------------------------------------------------
    # Agilo-specific configuration
    
    def test_days_are_fetched_correctly_from_config(self):
        """Regression test: Check that AgiloConfig uses the right configuration
        section and that use_days is really a bool, not a string."""
        self.trac_config.set('agilo-general', Key.USE_DAYS, False)
        self.assert_false(self.config.use_days)
        
        self.trac_config.set('agilo-general', Key.USE_DAYS, True)
        self.config.reload()
        self.assert_true(self.config.use_days)
    
    def test_can_enable_agilo_ui(self):
        self.config.enable_agilo_ui(save=True)
        self.assert_true(self.config.is_agilo_ui_enabled)
        self.config.disable_agilo_ui(save=True)
        self.assert_none(self.config.get('templates_dir', 'inherit'))
        self.assert_false(self.config.is_agilo_ui_enabled)
    
    def test_can_enable_agilo(self):
        self.config.enable_agilo()
        self.assert_true(self.config.is_agilo_enabled)
        self.config.disable_agilo()
        self.assert_false(self.config.is_agilo_enabled)
    
    def test_can_disable_agilo_ui(self):
        self.assert_true(self.config.is_agilo_ui_enabled)
        self.config.disable_agilo_ui(save=True)
        self.assert_false(self.config.is_agilo_ui_enabled)
    
    def _set_template_dir(self, config, dirname):
        config.change_option('templates_dir', dirname, 'inherit', save=True)
    
    def test_configuration_detects_outdated_template_path(self):
        self.assert_true(self.config.is_agilo_enabled)
        self.assert_true(self.config.is_agilo_ui_enabled)
        
        current_dir = '/usr/share/agilo-0.42-r12345.egg/templates'
        self._set_template_dir(self.config, '')
        self.assert_false(self.config.is_agilo_ui_enabled)
        self.assert_true(self.config._is_template_dir_outdated(current_dir))
        
        self._set_template_dir(self.config, '/my/user/configured/template')
        self.assert_false(self.config._is_template_dir_outdated(current_dir))
        
        self._set_template_dir(self.config, current_dir.replace('12345', '54321'))
        self.assert_true(self.config._is_template_dir_outdated(current_dir))
    
    def test_knows_when_filtered_burndown_is_enabled(self):
        self.assert_false(self.config.is_filtered_burndown_enabled())
        self.config.change_option('should_reload_burndown_on_filter_change_when_filtering_by_component', True, section=AgiloConfig.AGILO_GENERAL)
        self.assert_false(self.config.is_filtered_burndown_enabled())
        self.config.change_option('backlog_filter_attribute', 'component', section=AgiloConfig.AGILO_GENERAL)
        self.assert_true(self.config.is_filtered_burndown_enabled())
    
    # --------------------------------------------------------------------------
    # modify low-level configuration
    
    def test_can_remove_whole_sections(self):
        section = self.config.get_section('fnord')
        section.change_option('foo', 'bar')
        self.assert_true('fnord' in self.trac_config.sections())
        self.assert_equals('bar', section.get('foo'))
        section.remove()
        self.assert_not_equals('bar', section.get('foo'))
    
    def test_can_remove_sections_without_getting_it_first(self):
        section = self.config.get_section('fnord')
        section.change_option('foo', 'bar')
        self.assert_true('fnord' in self.trac_config.sections())
        self.assert_equals('bar', section.get('foo'))
        self.config.remove(section='fnord')
        self.assert_false(self.config.get_section('fnord').has_option('foo'))
        self.assert_not_equals('bar', section.get('foo'))
    
    def test_config_knows_if_an_option_is_set(self):
        self.assert_false(self.config.has_option('foo', section='fnord'))
        self.config.change_option('foo', 'bar', section='fnord')
        self.assert_true(self.config.has_option('foo', section='fnord'))
    
    def test_config_reloads_on_change(self):
        self.config.change_option('%s.%s' % (Type.BUG, Key.ALIAS),
                                  'Bugone', section=AgiloConfig.AGILO_TYPES,
                                  save=True)
        self.assert_equals('Bugone', self.config.ALIASES.get(Type.BUG))
    
    def test_config_reloads_links_configuration_on_change(self):
        self.assert_contains('story', LinksConfiguration(self.env).get_allowed_destination_types('requirement'))
        self.config.change_option('allow',
                                  '', section=AgiloConfig.AGILO_LINKS,
                                  save=True)
        self.assert_not_contains('story', LinksConfiguration(self.env).get_allowed_destination_types('requirement'))
    
    def test_config_writing_key_with_capitals(self):
        my_section = self.config.get_section('my-section')
        my_section.change_option('TestMe', 'This is a test', save=True)
        # Test that it is stored
        self.assert_equals('This is a test', 
                         self.env.config.get('my-section', 'TestMe'))
        # Test that is case insensitive
        self.assert_equals('This is a test', 
                         self.env.config.get('my-section', 'testme'))
        self.assert_equals('This is a test', 
                         self.env.config.get('my-section', 'TESTME'))
    
    def test_config_is_normalizing(self):
        my_section = self.config.get_section('my-section')
        my_section.change_option('TestMe', 'This is a test', save=True)
        # check that in reality only the lowecased version is saved in the
        # config file trac.ini
        options = self.config.get_options('my-section')
        self.assert_true('testme' in options,
                        "TestMe not found in: %s" % options)
        self.assert_false('TestMe' in options,
                        "TestMe found in: %s" % options)
    
    def test_config_not_updating_case_sensitive(self):
        my_section = self.config.get_section('my-section')
        my_section.change_option('TestMe', 'This is a test', save=True)
        # Test that it is asymmetric
        # Using set will not set the option as it is case insensitive and is
        # not stored because testme already exists in the trac.ini
        my_section.set_option('TESTME', 'This is another test', save=True)
        self.assert_not_equals('This is another test', 
                            self.env.config.get('my-section', 'TESTME'))
        self.assert_equals('This is a test', 
                         self.env.config.get('my-section', 'TESTME'))
        options = self.config.get_options('my-section')
        self.assert_true('testme' in options,
                         'TestMe not found in: %s' % options)
        self.assert_false('TestMe' in options,
                          'TestMe found in: %s' % options)
    
    def test_config_is_case_insensitive_and_overwrites(self):
        my_section = self.config.get_section('my-section')
        my_section.change_option('TestMe', 'This is a test', save=True)
        # Now change the option and check that also the old key, that is the
        # same actually changed
        my_section.change_option('TESTME', 'This is another test', save=True)
        self.assert_equals('This is another test', 
                         self.env.config.get('my-section', 'TESTME'))
        self.assert_not_equals('This is a test', 
                            self.env.config.get('my-section', 'testme'))
        # Now check what it is stored
        options = self.config.get_options('my-section')
        self.assert_true('testme' in options, 'testme not found in: %s' % options)
        self.assert_false('TESTME' in options, 'TESTME found in: %s' % options)
        # Test it is in the AgiloWrapper also after reload
        self.config.reload()
        self.assert_equals('This is another test', 
                         self.config.get('TestMe', 'my-section'))
    
    def test_config_stores_none_as_empty_string(self):
        my_section = self.config.get_section('my-section')
        my_section.set_option('test', 'This is a test', save=True)
        self.assert_equals('This is a test', 
                         self.env.config.get('my-section', 'test'))
        # Now change the option and check that also the old key, that is the
        # same actually changed
        my_section.change_option('test', None, save=True)
        self.assert_not_equals('This is a test', 
                               self.env.config.get('my-section', 'test'))
        self.assert_equals('', self.env.config.get('my-section', 'test'))
        # Check real config
        self.env.config.set('my-section', 'test', None)
        self.env.config.save()
        self.assert_equals('', self.env.config.get('my-section', 'test'))
    
    def test_dont_strip_non_string_values(self):
        self.assert_true(self.config.get('foo', default=True, section='trac'))
    
    def test_sets_default_agilo_logo_on_new_install(self):
        # should be set by the initialization so let's check it
        agilo_logo_src = 'agilo/images/default_logo.png'
        self.assert_equals(agilo_logo_src, self.env.config.get('header_logo', 'src'))
    
    def test_do_not_sets_default_logo_if_changed(self):
        test_src = 'my_logo'
        agilo_config = AgiloConfig(self.env)
        header_logo = agilo_config.get_section('header_logo')
        header_logo.change_option('src', test_src)
        self.assert_equals(test_src, header_logo.get('src'))
        set_default_agilo_logo(agilo_config)
        self.assert_equals(test_src, header_logo.get('src'))
    
    def test_sets_agilo_favicon_on_new_install(self):
        agilo_favicon = 'agilo/images/favicon.ico'
        self.assert_equals(agilo_favicon, self.env.config.get('project', 'icon'))
    
    def test_doesnt_overwrite_custom_favicons(self):
        custom_favicon = 'fnord'
        project_section = AgiloConfig(self.env).get_section('project')
        project_section.change_option('icon', custom_favicon)
        self.assert_equals(custom_favicon, project_section.get('icon'))
        
        initialize_config(self.env, {})
        project_section = AgiloConfig(self.env).get_section('project')
        self.assert_equals(custom_favicon, project_section.get('icon'))
Example #15
0
class CustomFields(Component):
    """ These methods should be part of TicketSystem API/Data Model.
    Adds update_custom_field and delete_custom_field methods.
    (The get_custom_fields is already part of the API - just redirect here,
     and add option to only get one named field back.)
    """
    def __init__(self, *args, **kwargs):
        """Initialize the component and set a TracConfig"""
        self.ticket_custom = \
            AgiloConfig(self.env).get_section(AgiloConfig.TICKET_CUSTOM)
        
    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 _store_all_options_for_custom_field(self, customfield):
        added_keys = list()
        changed = False
        for key in customfield:
            if key == Key.NAME:
                continue
            elif key == Key.TYPE:
                config_key = customfield[Key.NAME]
            else:
                config_key = '%s.%s' % (customfield[Key.NAME], key)
            value = customfield[key]
            if isinstance(value, list):
                value = '|'.join(value)
            if value not in ['', None]:
                changed = True
                self.ticket_custom.change_option(config_key, value, save=False)
                added_keys.append(key)
        if changed:
            self._remove_old_keys(customfield[Key.NAME], added_keys)
            self.ticket_custom.save()
    
    def _del_custom_field_value(self, customfield, prop=None):
        """Deletes a property from a custom field"""
        if not prop:
            self.ticket_custom.remove_option(customfield[Key.NAME])
        else:
            self.ticket_custom.remove_option('%s.%s' % (customfield[Key.NAME], prop))
    
    def _validate_input(self, customfield, create):
        """Checks the input values and raises a TracError if severe problems
        are detected."""
        # Name, Type are required
        if not (customfield.get(Key.NAME) and customfield.get(Key.TYPE)):
            raise TracError("Custom field needs at least a name and type.")
        
        # Use lowercase custom fieldnames only
        f_name = unicode(customfield[Key.NAME]).lower()
        # Only alphanumeric characters (and [-_]) allowed for custom fieldname
        if re.search('^[a-z0-9-_]+$', f_name) == None:
            raise TracError("Only alphanumeric characters allowed for custom field name (a-z or 0-9 or -_).")
        # If Create, check that field does not already exist
        if create and self.ticket_custom.get(f_name):
            raise TracError("Can not create as field already exists.")
        
        # Check that it is a valid field type
        f_type = customfield[Key.TYPE]
        if not f_type in ('text', 'checkbox', 'select', 'radio', 'textarea'):
            raise TracError("%s is not a valid field type" % f_type)
        
        if (Key.ORDER in customfield) and (not str(customfield.get(Key.ORDER)).isdigit()):
            raise TracError("%s is not a valid number for %s" % (customfield.get(Key.ORDER), Key.ORDER))
        
        customfield[Key.NAME] = f_name
    
    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()
        # TODO: Check that you can change the type from select to something different
        # and the options are gone afterwards
    
    
    def _set_custom_field_value(self, customfield, prop=None):
        """Sets a value in the custom fields for a given property key"""
        config_key = value = None
        if prop:
            value = customfield.get(prop)
            if isinstance(value, list):
                value = '|'.join(value)
            config_key = '%s.%s' % (customfield[Key.NAME], prop)
        else:
            # Used to set the type
            config_key = customfield[Key.NAME]
            value = customfield[Key.TYPE]
        self.ticket_custom.change_option(config_key, value)
    
    def _remove_old_keys(self, fieldname, added_keys):
        for key in (Key.VALUE, Key.OPTIONS, Key.COLS, Key.ROWS):
            if key not in added_keys:
                self.ticket_custom.remove_option('%s.%s' % (fieldname, key), 
                                                 save=False)
    
    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()
Example #16
0
def set_config_value(env, name, section, new_value):
    agilo_config = AgiloConfig(env)
    old_value = agilo_config.get(name, section)
    agilo_config.change_option(name, new_value, section=section, save=True)
    return old_value
Example #17
0
class AgiloConfigTest(AgiloTestCase):
    def setUp(self):
        self.super()
        self.trac_config = self.env.config
        self.config = AgiloConfig(self.env)

    # --------------------------------------------------------------------------
    # Agilo-specific configuration

    def test_days_are_fetched_correctly_from_config(self):
        """Regression test: Check that AgiloConfig uses the right configuration
        section and that use_days is really a bool, not a string."""
        self.trac_config.set('agilo-general', Key.USE_DAYS, False)
        self.assert_false(self.config.use_days)

        self.trac_config.set('agilo-general', Key.USE_DAYS, True)
        self.config.reload()
        self.assert_true(self.config.use_days)

    def test_can_enable_agilo_ui(self):
        self.config.enable_agilo_ui(save=True)
        self.assert_true(self.config.is_agilo_ui_enabled)
        self.config.disable_agilo_ui(save=True)
        self.assert_none(self.config.get('templates_dir', 'inherit'))
        self.assert_false(self.config.is_agilo_ui_enabled)

    def test_can_enable_agilo(self):
        self.config.enable_agilo()
        self.assert_true(self.config.is_agilo_enabled)
        self.config.disable_agilo()
        self.assert_false(self.config.is_agilo_enabled)

    def test_can_disable_agilo_ui(self):
        self.assert_true(self.config.is_agilo_ui_enabled)
        self.config.disable_agilo_ui(save=True)
        self.assert_false(self.config.is_agilo_ui_enabled)

    def _set_template_dir(self, config, dirname):
        config.change_option('templates_dir', dirname, 'inherit', save=True)

    def test_configuration_detects_outdated_template_path(self):
        self.assert_true(self.config.is_agilo_enabled)
        self.assert_true(self.config.is_agilo_ui_enabled)

        current_dir = '/usr/share/agilo-0.42-r12345.egg/templates'
        self._set_template_dir(self.config, '')
        self.assert_false(self.config.is_agilo_ui_enabled)
        self.assert_true(self.config._is_template_dir_outdated(current_dir))

        self._set_template_dir(self.config, '/my/user/configured/template')
        self.assert_false(self.config._is_template_dir_outdated(current_dir))

        self._set_template_dir(self.config,
                               current_dir.replace('12345', '54321'))
        self.assert_true(self.config._is_template_dir_outdated(current_dir))

    def test_knows_when_filtered_burndown_is_enabled(self):
        self.assert_false(self.config.is_filtered_burndown_enabled())
        self.config.change_option(
            'should_reload_burndown_on_filter_change_when_filtering_by_component',
            True,
            section=AgiloConfig.AGILO_GENERAL)
        self.assert_false(self.config.is_filtered_burndown_enabled())
        self.config.change_option('backlog_filter_attribute',
                                  'component',
                                  section=AgiloConfig.AGILO_GENERAL)
        self.assert_true(self.config.is_filtered_burndown_enabled())

    # --------------------------------------------------------------------------
    # modify low-level configuration

    def test_can_remove_whole_sections(self):
        section = self.config.get_section('fnord')
        section.change_option('foo', 'bar')
        self.assert_true('fnord' in self.trac_config.sections())
        self.assert_equals('bar', section.get('foo'))
        section.remove()
        self.assert_not_equals('bar', section.get('foo'))

    def test_can_remove_sections_without_getting_it_first(self):
        section = self.config.get_section('fnord')
        section.change_option('foo', 'bar')
        self.assert_true('fnord' in self.trac_config.sections())
        self.assert_equals('bar', section.get('foo'))
        self.config.remove(section='fnord')
        self.assert_false(self.config.get_section('fnord').has_option('foo'))
        self.assert_not_equals('bar', section.get('foo'))

    def test_config_knows_if_an_option_is_set(self):
        self.assert_false(self.config.has_option('foo', section='fnord'))
        self.config.change_option('foo', 'bar', section='fnord')
        self.assert_true(self.config.has_option('foo', section='fnord'))

    def test_config_reloads_on_change(self):
        self.config.change_option('%s.%s' % (Type.BUG, Key.ALIAS),
                                  'Bugone',
                                  section=AgiloConfig.AGILO_TYPES,
                                  save=True)
        self.assert_equals('Bugone', self.config.ALIASES.get(Type.BUG))

    def test_config_reloads_links_configuration_on_change(self):
        self.assert_contains(
            'story',
            LinksConfiguration(
                self.env).get_allowed_destination_types('requirement'))
        self.config.change_option('allow',
                                  '',
                                  section=AgiloConfig.AGILO_LINKS,
                                  save=True)
        self.assert_not_contains(
            'story',
            LinksConfiguration(
                self.env).get_allowed_destination_types('requirement'))

    def test_config_writing_key_with_capitals(self):
        my_section = self.config.get_section('my-section')
        my_section.change_option('TestMe', 'This is a test', save=True)
        # Test that it is stored
        self.assert_equals('This is a test',
                           self.env.config.get('my-section', 'TestMe'))
        # Test that is case insensitive
        self.assert_equals('This is a test',
                           self.env.config.get('my-section', 'testme'))
        self.assert_equals('This is a test',
                           self.env.config.get('my-section', 'TESTME'))

    def test_config_is_normalizing(self):
        my_section = self.config.get_section('my-section')
        my_section.change_option('TestMe', 'This is a test', save=True)
        # check that in reality only the lowecased version is saved in the
        # config file trac.ini
        options = self.config.get_options('my-section')
        self.assert_true('testme' in options,
                         "TestMe not found in: %s" % options)
        self.assert_false('TestMe' in options, "TestMe found in: %s" % options)

    def test_config_not_updating_case_sensitive(self):
        my_section = self.config.get_section('my-section')
        my_section.change_option('TestMe', 'This is a test', save=True)
        # Test that it is asymmetric
        # Using set will not set the option as it is case insensitive and is
        # not stored because testme already exists in the trac.ini
        my_section.set_option('TESTME', 'This is another test', save=True)
        self.assert_not_equals('This is another test',
                               self.env.config.get('my-section', 'TESTME'))
        self.assert_equals('This is a test',
                           self.env.config.get('my-section', 'TESTME'))
        options = self.config.get_options('my-section')
        self.assert_true('testme' in options,
                         'TestMe not found in: %s' % options)
        self.assert_false('TestMe' in options, 'TestMe found in: %s' % options)

    def test_config_is_case_insensitive_and_overwrites(self):
        my_section = self.config.get_section('my-section')
        my_section.change_option('TestMe', 'This is a test', save=True)
        # Now change the option and check that also the old key, that is the
        # same actually changed
        my_section.change_option('TESTME', 'This is another test', save=True)
        self.assert_equals('This is another test',
                           self.env.config.get('my-section', 'TESTME'))
        self.assert_not_equals('This is a test',
                               self.env.config.get('my-section', 'testme'))
        # Now check what it is stored
        options = self.config.get_options('my-section')
        self.assert_true('testme' in options,
                         'testme not found in: %s' % options)
        self.assert_false('TESTME' in options, 'TESTME found in: %s' % options)
        # Test it is in the AgiloWrapper also after reload
        self.config.reload()
        self.assert_equals('This is another test',
                           self.config.get('TestMe', 'my-section'))

    def test_config_stores_none_as_empty_string(self):
        my_section = self.config.get_section('my-section')
        my_section.set_option('test', 'This is a test', save=True)
        self.assert_equals('This is a test',
                           self.env.config.get('my-section', 'test'))
        # Now change the option and check that also the old key, that is the
        # same actually changed
        my_section.change_option('test', None, save=True)
        self.assert_not_equals('This is a test',
                               self.env.config.get('my-section', 'test'))
        self.assert_equals('', self.env.config.get('my-section', 'test'))
        # Check real config
        self.env.config.set('my-section', 'test', None)
        self.env.config.save()
        self.assert_equals('', self.env.config.get('my-section', 'test'))

    def test_dont_strip_non_string_values(self):
        self.assert_true(self.config.get('foo', default=True, section='trac'))

    def test_sets_default_agilo_logo_on_new_install(self):
        # should be set by the initialization so let's check it
        agilo_logo_src = 'agilo/images/default_logo.png'
        self.assert_equals(agilo_logo_src,
                           self.env.config.get('header_logo', 'src'))

    def test_do_not_sets_default_logo_if_changed(self):
        test_src = 'my_logo'
        agilo_config = AgiloConfig(self.env)
        header_logo = agilo_config.get_section('header_logo')
        header_logo.change_option('src', test_src)
        self.assert_equals(test_src, header_logo.get('src'))
        set_default_agilo_logo(agilo_config)
        self.assert_equals(test_src, header_logo.get('src'))

    def test_sets_agilo_favicon_on_new_install(self):
        agilo_favicon = 'agilo/images/favicon.ico'
        self.assert_equals(agilo_favicon,
                           self.env.config.get('project', 'icon'))

    def test_doesnt_overwrite_custom_favicons(self):
        custom_favicon = 'fnord'
        project_section = AgiloConfig(self.env).get_section('project')
        project_section.change_option('icon', custom_favicon)
        self.assert_equals(custom_favicon, project_section.get('icon'))

        initialize_config(self.env, {})
        project_section = AgiloConfig(self.env).get_section('project')
        self.assert_equals(custom_favicon, project_section.get('icon'))