Example #1
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 #2
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 #3
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]