File: api.py

package info (click to toggle)
trac-customfieldadmin 0.4.0%2Bsvn18456-2~bpo12%2B1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm-backports
  • size: 196 kB
  • sloc: python: 524; javascript: 55; makefile: 4
file content (201 lines) | stat: -rw-r--r-- 9,489 bytes parent folder | download | duplicates (2)
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)