trac-customfieldadmin / 0.11 / customfieldadmin /

# -*- coding: utf-8 -*-
API for administrating custom ticket fields in Trac.
Supports creating, getting, updating and deleting custom fields.

License: BSD

(c) 2005-2012 ::: - BV Network AS (

import re

from pkg_resources import resource_filename

from trac.core import *
from trac.ticket.api import TicketSystem

    from  trac.util.translation import domain_functions
    add_domain, _, tag_ = \
        domain_functions('customfieldadmin', ('add_domain', '_', 'tag_'))
    # fall back to 0.11 behavior, i18n functions are no-ops then
    from genshi.builder import tag as tag_
    from trac.util.translation import gettext as _
    def add_domain(*args, **kwargs):

__all__ = ['CustomFields']

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.)
    Input to methods is a 'cfield' dict supporting these 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
        format = text|wiki (for text and textarea)
    config_options = ['label', 'value', 'options', 'cols', 'rows',
                         'order', 'format']
    def __init__(self):
        # bind the 'customfieldadmin' catalog to the specified locale directory
        locale_dir = resource_filename(__name__, 'locale')
        add_domain(self.env.path, locale_dir)
        # TODO: Remove systeminfo compat code when only supporting Trac>=0.12
            from trac.loader import get_plugin_info
        except ImportError:
            from customfieldadmin import __version__
            self.env.systeminfo.append(('CustomFieldAdmin', __version__))
    def get_custom_fields(self, cfield=None):
        """ Returns the custom fields from TicketSystem component.
        Use a cfdict with 'name' key set to find a specific custom field only.
        if not cfield:    # return full list
            return TicketSystem(self.env).get_custom_fields()
        else:                  # only return specific item with cfname
            all = TicketSystem(self.env).get_custom_fields()
            for item in all:
                if item['name'] == cfield['name']:
                    return item
            return None        # item not found
    def verify_custom_field(self, cfield, create=True):
        """ Basic validation of the input for modifying or creating
        custom fields. """
        # Name, Type and Label is required
        if not (cfield.get('name') and cfield.get('type') \
                    and cfield.get('label')):
            raise TracError(
                    _("Custom field needs at least a name, type and label."))
        # Use lowercase custom fieldnames only
        cfield['name'] = cfield['name'].lower()
        # Only alphanumeric characters (and [-_]) allowed for custom fieldname
        if'^[a-z][a-z0-9_]+$', cfield['name']) == None:
           raise TracError(_("Only alphanumeric characters allowed for " \
                             "custom field name ('a-z' or '0-9' or '_'), " \
                             "with 'a-z' as first character."))
        # Name must begin with a character - anything else not supported by Trac
        if not cfield['name'][0].isalpha():
            raise TracError(
                    _("Custom field name must begin with a character (a-z)."))
        # Check that it is a valid field type
        if not cfield['type'] in \
                        ['text', 'checkbox', 'select', 'radio', 'textarea']:
            raise TracError(_("%(field_type)s is not a valid field type"),
        # Check that field does not already exist
        # (if modify it should already be deleted)
        if create and self.config.get('ticket-custom', cfield['name']):
            raise TracError(_("Can not create as field already exists."))
    def create_custom_field(self, cfield):
        """ Create the new custom fields (that may just have been deleted as
        part of 'modify'). Note: Caller is responsible for verifying input
        before create."""
        # Set the mandatory items
        self.config.set('ticket-custom', cfield['name'], cfield['type'])
        self.config.set('ticket-custom', cfield['name'] + '.label',
        # Optional items
        if 'value' in cfield:
            self.config.set('ticket-custom', cfield['name'] + '.value',
        if 'options' in cfield:
            if cfield.get('optional', False):
                self.config.set('ticket-custom', cfield['name'] + '.options',
                                '|' + '|'.join(cfield['options']))
                self.config.set('ticket-custom', cfield['name'] + '.options',
        if 'format' in cfield and cfield['type'] in ('text', 'textarea'):
            self.config.set('ticket-custom', cfield['name'] + '.format',
        # Textarea
        if cfield['type'] == 'textarea':
            cols = cfield.get('cols') and int(cfield.get('cols', 0)) > 0 \
                                                and cfield.get('cols') or 60
            rows = cfield.get('rows', 0) and int(cfield.get('rows', 0)) > 0 \
                                                and cfield.get('rows') or 5
            self.config.set('ticket-custom', cfield['name'] + '.cols', cols)
            self.config.set('ticket-custom', cfield['name'] + '.rows', rows)
        # Order
        order = cfield.get('order', "")
        if order == "":
            order = len(self.get_custom_fields())
        self.config.set('ticket-custom', cfield['name'] + '.order', order)

    def update_custom_field(self, cfield, create=False):
        """ Updates a custom. Option to 'create' is kept in order to keep
        the API backwards compatible. """
        if create:
        # Check input, then delete and save new
        self.verify_custom_field(cfield, create=False)
        self.delete_custom_field(cfield, modify=True)
    def delete_custom_field(self, cfield, modify=False):
        """ Deletes a custom field. Input is a dictionary
        (see update_custom_field), but only ['name'] is required.
        if not self.config.get('ticket-custom', cfield['name']):
            return # Nothing to do here - cannot find field
        if not modify:
            # Permanent delete - reorder later fields to lower order
            order_to_delete = self.config.getint('ticket-custom',
            cfs = self.get_custom_fields()
            for field in cfs:
                if field['order'] > order_to_delete:
                    self.config.set('ticket-custom', field['name']+'.order',
                                                    field['order'] -1 )
        supported_options = [cfield['name']] + \
            [cfield['name']+'.'+opt for opt in self.config_options]
        for option, _value in self.config.options('ticket-custom'):
            if modify and not option in supported_options:
                # Only delete supported options when modifying
            if option == cfield['name'] \
                    or option.startswith(cfield['name'] + '.'):
                self.config.remove('ticket-custom', option)
        # Persist permanent deletes
        if not modify:

    def _save(self):
        # Saves a value, clear caches if needed / supported
            # cache support for Trac >= 0.12
            del TicketSystem(self.env).custom_fields
        except AttributeError:
            # 0.11 cached values internally
            TicketSystem(self.env)._custom_fields = None