1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
|
# -*- coding: utf-8 -*-
"""
API for administrating custom ticket fields in Trac.
Supports creating, getting, updating and deleting custom fields.
License: BSD
(c) 2005-2012 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no)
"""
import pkg_resources
import re
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_'))
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 (ascii alphanumeric only)
type = text|checkbox|select|radio|textarea|time
label = label description
value = default value for field content
options = options for select and radio types
(list, leave first empty for optional)
rows = number of rows for text area
order = specify sort order for field
format = text|wiki|reference|list (for text)
format = text|wiki (for textarea)
format = relative|date|datetime (for time)
"""
config_options = ['label', 'value', 'options', 'rows', 'order', 'format']
field_types = ['text', 'checkbox', 'select', 'radio', 'textarea', 'time']
field_formats = {
'text': ['plain', 'wiki', 'reference', 'list'],
'textarea': ['plain', 'wiki'],
'time': ['relative', 'date', 'datetime'],
}
def __init__(self):
# bind the 'customfieldadmin' catalog to the specified locale directory
try:
locale_dir = pkg_resources.resource_filename(__name__, 'locale')
except KeyError:
# No 'locale', hence no compiled message catalogs. Ignore.
pass
else:
add_domain(self.env.path, locale_dir)
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.
"""
items = TicketSystem(self.env).get_custom_fields()
for item in items:
if item['type'] == 'textarea':
item['rows'] = item.pop('height')
if cfield and item['name'] == cfield['name']:
return item # only return specific item with cfname
if cfield:
return None # item not found
else:
return items # return full list
def verify_custom_field(self, cfield, create=True):
""" Basic validation of the input for modifying or creating
custom fields. """
# Requires 'name' and 'type'
if not (cfield.get('name') and cfield.get('type')):
raise TracError(
_("Custom field requires attributes 'name' and 'type'."))
# Use lowercase custom fieldnames only
cfield['name'] = cfield['name'].lower()
# Check field name is not reserved
tktsys = TicketSystem(self.env)
if cfield['name'] in tktsys.reserved_field_names:
raise TracError(_("Field name '%(name)s' is reserved",
name=cfield['name']))
# Only alphanumeric characters (and [-_]) allowed for custom fieldname
if re.search('^[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 self.field_types:
raise TracError(_("%(field_type)s is not a valid field type",
field_type=cfield['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."))
if create and [f for f in tktsys.fields
if f['name'] == cfield['name']]:
raise TracError(_("Can't create a custom field with the "
"same name as a built-in field."))
def create_custom_field(self, cfield):
""" Create the new custom fields (that may just have been deleted as
part of 'modify'). In `cfield`, 'name' and 'type' keys are required.
Note: Caller is responsible for verifying input before create."""
# Need count pre-create for correct order
count_current_fields = len(self.get_custom_fields())
# Set the mandatory items
self.config.set('ticket-custom', cfield['name'], cfield['type'])
# Label = capitalize fieldname if not present
self.config.set('ticket-custom', cfield['name'] + '.label',
cfield.get('label') or cfield['name'].capitalize())
# Optional items
if 'value' in cfield:
self.config.set('ticket-custom', cfield['name'] + '.value',
cfield['value'])
if cfield['type'] in ('select', 'radio') and \
'options' in cfield:
if cfield.get('optional', False) and '' not in cfield['options']:
self.config.set('ticket-custom', cfield['name'] + '.options',
'|' + '|'.join(cfield['options']))
else:
self.config.set('ticket-custom', cfield['name'] + '.options',
'|'.join(cfield['options']))
if 'format' in cfield and \
cfield['type'] in ('text', 'textarea', 'time') and \
cfield['format'] in self.field_formats[cfield['type']]:
self.config.set('ticket-custom', cfield['name'] + '.format',
cfield['format'])
# Textarea
if cfield['type'] == 'textarea':
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'] + '.rows', rows)
# Order
order = cfield.get('order') or count_current_fields + 1
self.config.set('ticket-custom', cfield['name'] + '.order', order)
self._save(cfield)
def update_custom_field(self, cfield, create=False):
""" Updates a custom field. Option to 'create' is kept in order to keep
the API backwards compatible. """
if create:
self.verify_custom_field(cfield)
self.create_custom_field(cfield)
return
# Check input, then delete and save new
if not self.get_custom_fields(cfield=cfield):
raise TracError(_("Custom Field '%(name)s' does not exist. " \
"Cannot update.", name=cfield.get('name') or '(none)'))
self.verify_custom_field(cfield, create=False)
self.delete_custom_field(cfield, modify=True)
self.create_custom_field(cfield)
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',
cfield['name']+'.order')
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
# http://trac-hacks.org/ticket/8188
continue
if option == cfield['name'] \
or option.startswith(cfield['name'] + '.'):
self.config.remove('ticket-custom', option)
# Persist permanent deletes
if not modify:
self._save(cfield)
def _save(self, cfield=None):
""" Saves a value, clear caches if needed / supported. """
self.config.save()
del TicketSystem(self.env).custom_fields
# Re-populate contents of cfield with new values and defaults
if cfield:
stored = self.get_custom_fields(cfield=cfield)
if stored: # created or updated (None for deleted so just ignore)
cfield.update(stored)
|