# IssueTrackerProduct
#
# Peter Bengtsson <mail@peterbe.com>
# License: ZPL
#
# python
import os, sys, re
import logging
from sets import Set
from types import InstanceType
# Zope
from OFS.Folder import Folder
from Globals import DevelopmentMode
from AccessControl import ClassSecurityInfo
from Acquisition import aq_inner,aq_parent,aq_base
from DateTime import DateTime
from DateTime.DateTime import DateError
try:
# >= Zope 2.12
from App.special_dtml import DTMLFile
from App.class_init import InitializeClass
except ImportError:
# < Zope 2.12
from Globals import DTMLFile,InitializeClass
try:
from persistent.mapping import PersistentMapping
except ImportError:
# for old versions of Zope
PersistentMapping = dict
# Product
import Utils
from Utils import unicodify
from TemplateAdder import addTemplates2Class
from Constants import *
from Permissions import VMS
from Expression import Expression,getExprContext
#----------------------------------------------------------------------------
DEFAULT_TEXTAREA_COLS = 70
DEFAULT_TEXTAREA_ROWS = 10
logger = logging.getLogger('IssueTrackerProduct.CustomField')
OK_input_types = ('text', 'password', 'checkbox', 'textarea', 'select',
'radio', 'file')
OK_python_types = (
'boolean',
'date',
'float',
'int',
'lines',
'long',
'string',
'ulines',
'ustring',
)
CORE_ATTRIBUTES = ('title','input_type','extra_css','extra_js','options',
'mandatory','options_expression','python_type',
'visibility_expression',
'include_in_filter_options',
)
#----------------------------------------------------------------------------
manage_addCustomFieldForm = DTMLFile('dtml/addCustomField', globals())
def manage_addCustomField(self, oid, title=u'', input_type='text', python_type='ustring',
extra_css=u'', extra_js=u'', mandatory=False,
include_in_filter_options=False,
options=[],
create_in_folder=False,
add_and_edit=False, REQUEST=None):
""" This adds a custom field object inside a CustomFieldFolder.
A custom field is best added inside a custom field folder object but it's
not a must.
"""
# by default the place to put it is in self
container = self
if create_in_folder:
# either find or create a CustomFieldFolder
if self.meta_type == CUSTOMFIELDFOLDER_METATYPE:
# great!
pass
elif self.objectValues(CUSTOMFIELDFOLDER_METATYPE):
container = self.objectValues(CUSTOMFIELDFOLDER_METATYPE)[0]
else:
# create one and put it in that
container = manage_addCustomFieldFolder(self)
if input_type != 'text':
# check that it's a valid on
if not input_type in OK_input_types:
raise ValueError, "invalid input_type"
#if input_type in ('select','radio'):
# assert options, "No options set when using select"
if not python_type in OK_python_types:
raise ValueError, "Invalid python type"
instance = CustomField(oid, title=unicode(title), input_type=input_type,
python_type=python_type,
extra_css=unicode(extra_css).strip(),
extra_js=unicode(extra_js).strip(),
mandatory=bool(mandatory),
options=options,
include_in_filter_options=include_in_filter_options,
)
container._setObject(oid, instance)
object = container._getOb(oid)
object._prepareByType()
if REQUEST is not None:
url = container.absolute_url() + '/manage_main'
if add_and_edit:
url = object.absolute_url() + '/manage_field'
REQUEST.RESPONSE.redirect(url)
else:
return object
try:
sorted
except NameError:
# python2.3
# This hack is limited as it doesn't support the same
# parameters as builtin sorted()
def sorted(iterable):
copy = iterable[:]
copy.sort()
return copy
def list_to_flat(sequence):
items = []
for item in sequence:
if isinstance(item, (list, tuple)):
items.append(u'%s | %s' % (item[0], item[1]))
else:
items.append(item)
return u'\n'.join(items)
def flat_to_list(string):
# fist convert it to a decent list
flat_list = [x.strip() for x in string.splitlines() if x.strip()]
items = []
for item in flat_list:
if len(item.split('|')) == 2:
items.append([x.strip() for x in item.split('|')])
else:
items.append(item.strip())
return items
def compare_custom_value(value1, value2, python_type):
if python_type == 'int':
try:
return int(value1) == int(value2)
except ValueError:
pass
elif python_type == 'float':
try:
return float(value1) == float(value2)
except ValueError:
pass
# most basic comparison
return value1 == value2
#----------------------------------------------------------------------------
class CustomField(Folder):
"""
A CustomField is an object that becomes automatically included
as part of the Add Issue page. The ID of the custom field becomes the
name of the input. So if the ID is 'foo' the input rendered becomes
<input name="foo">
This class defines:
Type of input
---------------------------------------------------------------------------
You can select one of the following:
text, textarea, password, hidden, select, checkbox, radio or file
Depending on which one you select you'll specify parameters such as
'cols' (for type 'textarea' of course) or size. By having first selected
a type, the field will autogenerate some default parameters that you can
later modify.
Default value
---------------------------------------------------------------------------
The default value can be either a simple string inputted or it can be a
reference to something else callable that will get the default value and
this is done with a TALES expression.
Being mandatory or optional
---------------------------------------------------------------------------
By default every field is optional but by making it mandatory, you'll
most likely going to have to specify a validation because sometimes it's
not as simple as checking that a value is boolean or not (e.g. bool(''))
Validation
---------------------------------------------------------------------------
This is where you specify either a reference to a script or a TALES
expression that will work out if a particular value is valid or not.
Javascript events hooks (onchange, onclick, onfocus, onblur)
---------------------------------------------------------------------------
You'll be responsible for what you write in the values for these. The
values must be available javascript functions.
Setting persistent values on issues
---------------------------------------------------------------------------
(This is actually implemented in IssueTrackerProduct/IssueTracker.py)
When saving the issue, we'll add an attribute to the issue like this::
<id of custom field>: <value at input>
This will pass through the validation a second time but unlike the first
time, if the validation fails this time a hard error is raised. The type
of the value is by default a unicode string or what else is appropriate
based on the input type.
You can specify an expression that will massage the input before it's
saved. So, suppose you want to save it as a floating point number you
enter this expression::
python:float(value)
Getting persistent values on issues
---------------------------------------------------------------------------
(This is actually implemented in IssueTrackerProduct/IssueTracker.py)
You can ask the issuetracker for the value of a custom field simply by
specifying the ID of the custom field and an optional default value.
Quite possibly you'll have an issuetracker where issues were added before
the creation of the custom field so it'll be important to supply a
default value.
Additionally loaded Javascript and CSS
---------------------------------------------------------------------------
You get an area for entering the Javascript and the CSS and this is
automatically loaded on the Add Issue page. If you in your input of this
(on the first line) enter a name of a file or DTML Method/Document that
exists, that is instead rendered.
The input can also be a valid URL if it looks relative and valid.
"""
meta_type = CUSTOMFIELD_METATYPE
manage_options = ({'label':'Manage', 'action':'manage_field'},
{'label':'Validation', 'action':'manage_validation'},) +\
Folder.manage_options
_properties = ({'id':'title', 'type': 'ustring', 'mode':'w'},
{'id':'disabled', 'type': 'boolean', 'mode':'w'},
{'id':'python_type', 'type': 'selection', 'mode':'w',
'select_variable':'getOKPythonTypes'},
{'id':'include_in_filter_options', 'type': 'boolean', 'mode':'w'},
)
security = ClassSecurityInfo()
def __init__(self, id, title=u'', input_type="text", python_type='ustring',
extra_js=u'', extra_css=u'', mandatory=False,
options=[], options_expression='', visibility_expression='',
include_in_filter_options=False):
self.id = str(id)
self.title = title
self.input_type = input_type
self.python_type = python_type
self.attributes = PersistentMapping()
self.extra_css = extra_css
self.extra_js = extra_js
self.mandatory = mandatory
self.options = options
self.options_expression = options_expression
self.disabled = False
self.visibility_expression = visibility_expression
self.include_in_filter_options = include_in_filter_options
##
## Attributes of the object
##
def getId(self):
return self.id
def getTitle(self):
return self.title
def isMandatory(self):
return self.mandatory
def isDisabled(self):
return self.disabled
def getOptions(self):
return self.options
def getInputType(self):
return self.input_type
def getPythonType(self):
return self.python_type
security.declareProtected(VMS, 'getOptionsFlat')
def getOptionsFlat(self):
""" return the list of options with a | pipe sign to split tuples """
return list_to_flat(self.getOptions())
def getOptionsExpression(self):
""" true if it looks like a TALES expression """
return self.options_expression
def getVisibilityExpression(self):
return self.visibility_expression
def includeInFilterOptions(self):
return self.include_in_filter_options
##
## Special Zope magic
##
def getOKPythonTypes(self):
return OK_python_types
##
## Special massaging on the class attributes
##
def _prepareByType(self):
""" set all the appropriate default bits and pieces by the
input_type. For example, if the input type is 'textarea' set a
default cols and rows.
"""
if self.input_type == 'textarea':
self.attributes['cols'] = DEFAULT_TEXTAREA_COLS
self.attributes['rows'] = DEFAULT_TEXTAREA_ROWS
elif self.input_type == 'checkbox':
pass
#if 'value' in self.attributes:
# del self.attributes['value']
elif self.input_type == 'radio':
if 'value' in self.attributes:
del self.attributes['value']
elif self.input_type == 'file':
if 'value' in self.attributes:
del self.attributes['value']
##
## Rendering stuff
##
def render(self, *value, **extra_attributes):
""" return the tag (e.g. <textarea>) and any other accompanying
HTML stuff.
"""
# if someone passes None as the first and only parameter to render()
# the value of variable 'value' will be (None,)
# This should be considered as if nothing is set
if value == (None,):
value = []
if value and isinstance(value[0], InstanceType) and \
value[0].__class__.__name__ =='HTTPRequest':
# this method has been called with REQUEST as the value parameter.
# Note that it's still a list or tuple but convert it to the actual value.
value = value[0].form.get(self.getId(), None)
if value is None:
value = ()
else:
value = (value,) # make sure it's a tuple
out = []
if DevelopmentMode:
out.append(u'<!--CustomField: %s -->' % self.getId())
if self.isDisabled():
logger.warn("A disabled custom field (%s) is rendered" % self.absolute_url_path())
# take out some extra keywords from the extra_attributes
skip_extra_css = extra_attributes.pop('skip_extra_css', False)
skip_extra_js = extra_attributes.pop('skip_extra_js', False)
if self.extra_css and not skip_extra_css:
out.append(self.render_extra_css())
if self.extra_js and not skip_extra_js:
out.append(self.render_extra_js())
out.append(self.render_tag(*value, **extra_attributes))
return '\n'.join(out)
def render_tag(self, *value, **extra_attributes):
""" return a piece of unicode HTML that """
assert len(value) <= 1, "Can't pass more than one argument as value"
inner = []
attributes = {}
# notice the order of these update() calls! It matters
name_prefix = extra_attributes.pop('name_prefix','')
# It's an option to called render_tag() with in_filter=True which tells
# us that this tag is rendered as a filter, in the filter options.
# This is something that can be done on-the-fly and it means that
# certain things should work differently. For example, a 'select' type
# input get's an added 'size' and 'multiple' attribute when used as a
# filter.
in_filter = extra_attributes.pop('in_filter', False)
# core attributes
dom_id = self.attributes.get('dom_id', 'id_%s' % self.getId())
attributes.update({'name':self._wrapPythonTypeName(name_prefix),
'id':dom_id,
'title':self.getTitle()
})
# saved attributes
attributes.update(dict(self.attributes))
# extra on rendering attributes
attributes.update(extra_attributes)
# Now, "hack" the attributes if this is used in a filter
if in_filter:
if self.input_type == 'select':
attributes['multiple'] = 'multiple'
if 'size' not in attributes:
attributes['size'] = min(5, len(list(self.getOptionsIterable())))
# filler is a dict that we will use to render the template
filler = {}
if self.input_type == 'textarea':
template = u'<textarea %(inner)s>%(value)s</textarea>'
v = None
if value:
v = value[0] # from the argument
elif 'value' in attributes:
v = attributes.pop('value')
if v:
filler['value'] = Utils.safe_html_quote(v)
else:
filler['value'] = u''
elif self.input_type == 'select':
template = u'<select %(inner)s>\n%(all_options)s\n</select>'
all_options = []
v = []
if value:
v = value[0]
# makes sure the value doesn't become a nested list
if isinstance(v, list):
v = Utils.flatten_lines(v)
elif 'value' in attributes:
v = attributes.pop('value')
if not isinstance(v, (tuple, list)):
v = [v]
# if the value passed to render this select contains
# items that are not in the list of options, don't
# use the list of options.
_values_selected = []
for option in self.getOptionsIterable():
if isinstance(option, (tuple, list)):
value, label = option
else:
value, label = option, option
if self.getPythonType() == 'int':
try:
value = int(value)
except ValueError:
pass
elif self.getPythonType() == 'float':
try:
value = float(value)
except ValueError:
pass
if value in v:
tmpl = u'<option value="%s" selected="selected">%s</option>'
_values_selected.append(value)
else:
tmpl = u'<option value="%s">%s</option>'
all_options.append(tmpl % (value, label))
if Set(v) - Set(_values_selected):
# there were values that weren't in the list of options!
_values_not_in_options = list(Set(v) - Set(_values_selected))
# if nothing was matched in the list of options,
# reset the whole all_options list.
if not _values_selected and all_options:
all_options = []
for value in _values_not_in_options:
label = value
tmpl = u'<option value="%s" selected="selected">%s</option>'
all_options.append(tmpl % (value, label))
filler['all_options'] = '\n'.join(all_options)
elif self.input_type == 'radio':
# special case
if not self.getOptionsIterable():
template = u'ERROR: No options'
else:
template = u'%(all_inputs)s'
all_inputs = []
v = None
if value:
v = value[0] # from the argument
elif 'value' in attributes:
v = attributes.pop('value')
special_attributes = ''
inner = []
for k, v2 in attributes.items():
if k in ('id',):
continue
inner.append('%s="%s"' % (k, v2))
if inner:
special_attributes = ' ' + ' '.join(inner)
for option in self.getOptions():
if isinstance(option, (tuple, list)):
value, label = option
else:
value, label = option, option
if value == v:
tmpl = u'<input type="radio" value="%s" checked="checked"%s /> %s<br />'
else:
tmpl = u'<input type="radio" value="%s"%s/> %s<br />'
all_inputs.append(tmpl % (value, special_attributes, label))
filler['all_inputs'] = '\n'.join(all_inputs)
elif self.input_type == 'checkbox':
# another special case
v = None
if value:
v = value[0] # from the argument
elif 'value' in attributes:
v = attributes.pop('value')
# If there are no options you can work this like a normal text input
if not self.getOptions():
# but what if it should be a boolean and it's true, then the
# tag needs to contain checked="checked"
if v:
template = u'<input type="checkbox" checked="checked" %(inner)s />'
else:
template = u'<input type="checkbox" %(inner)s />'
else:
# crap!
template = u'%(all_inputs)s'
all_inputs = []
special_attributes = ''
inner = []
for k, v2 in attributes.items():
if k in ('id',):
continue
inner.append('%s="%s"' % (k, v2))
if inner:
special_attributes = ' ' + ' '.join(inner)
for option in self.getOptions():
if isinstance(option, (tuple, list)):
value, label = option
else:
value, label = option, option
if value == v:
tmpl = u'<input type="checkbox" value="%s" checked="checked"%s /> %s<br />'
else:
tmpl = u'<input type="checkbox" value="%s"%s/> %s<br />'
all_inputs.append(tmpl % (value, special_attributes, label))
filler['all_inputs'] = '\n'.join(all_inputs)
elif self.input_type == 'password':
template = u'<input type="password" %(inner)s />'
elif self.input_type == 'file':
template = u'<input type="file" %(inner)s />'
else: # type text
template = u'<input %(inner)s />'
if not (self.input_type == 'radio' or (self.input_type == 'checkbox' and self.getOptions())):
if value and self.input_type not in ('select',):
if value and value[0]:
# This overrides the default value
attributes['value'] = value[0]
for key, val in sorted(attributes.items()):
inner.append('%s="%s"' % (key, val))
filler['inner'] = ' '.join(inner)
return template % filler
def __str__(self):
return str(self.render())
def _wrapPythonTypeName(self, prefix=''):
""" if name is 'age' and python_type is 'int' then return
'age:int'.
If the type is unicode type, add the encoding
"""
name, python_type = self.getId(), self.python_type
# add the prefix
name = '%s%s' % (prefix, name)
if self.input_type == 'file':
# exception
return name
if python_type in ('ustring','ulines'):
return '%s:%s:%s' % (name, UNICODE_ENCODING, python_type)
elif python_type == 'string':
return name
else:
return '%s:%s' % (name, python_type)
def render_extra_css(self):
""" return a piece of HTML that loads the CSS.
If it looks like the attribute self.extra_css is a URI,
return a <link rel="stylesheet"> tag instead.
"""
css = self.extra_css
if len(css.splitlines()) == 1 and (css.startswith('http') or css.startswith('/') or css.endswith('.css')):
return u'<link rel="stylesheet" type="text/css" href="%s" />' % css
elif css:
return u'<style type="text/css">\n%s\n</style>' % css
else:
return u''
def render_extra_js(self):
""" return a piece of HTML that loads the javascript.
If it looks like the attribute self.extra_js is a URI,
return a <script src="..."> tag instead.
"""
js = self.extra_js
if len(js.splitlines()) == 1 and (js.startswith('http') or js.startswith('/') or js.endswith('.js')):
return u'<script type="text/javascript" src="%s"></script>' % js
elif js:
return u'<script type="text/javascript">\n%s\n</script>' % js
else:
return u''
security.declareProtected(VMS, 'preview_render')
def preview_render(self, *value, **extra_attributes):
""" wrapper on render() that is able to cut out some of the verbose stuff
from the render output.
"""
html = self.render(*value, **extra_attributes)
return html
##
## TALES expression for options
##
def getOptionsIterable(self):
""" return a list of options """
if self.getOptionsExpression():
ec = self._getExprContext(self)
ex = Expression(self.options_expression)
return list(ex(ec))
else:
return self.getOptions()
def _getExprContext(self, object, extra_namespaces={}):
return getExprContext(self, object, extra_namespaces=extra_namespaces)
def _valid_options_expression(self):
""" return true if self.options_expression is valid
otherwise raise an error.
"""
ec = self._getExprContext(self)
ex = Expression(self.options_expression)
iterable = ex(ec)
if isinstance(iterable, (list, tuple)):
# each item should be unicodeable and
# every item must something
for item in iterable:
if isinstance(item, (tuple, list)):
key, value = item
if key and not value:
value = key
else:
key, value = item, item
if not item:
return False
# an iterable we can't find anything wrong with
return True
# default is not to pass
return False
##
## Validation
##
def getValidationExpressions(self):
return self.objectValues(CUSTOMFIELD_VALIDATION_EXPRESSION_METATYPE)
security.declarePrivate('testValidValue')
def testValidValue(self, value):
""" return a tuple of (valid or not [bool], message [unicode]) if the value
passes all the validation expressions (assuming the field has any)
"""
# check the python type
if self.python_type == 'ustring':
# should be possible to do this
try:
unicode(value)
except TypeError:
return False, u"Not a unicode string"
elif self.python_type == 'int':
try:
int(value)
except ValueError:
return False, u"Not an integer number"
elif self.python_type == 'float':
try:
float(value)
except ValueError:
return False, u"Not a floating point number"
elif self.python_type == 'long':
try:
long(value)
except ValueError:
return False, u"Not a long integer number"
elif self.python_type == 'date':
try:
if isinstance(value, basestring):
DateTime(value)
except DateError:
return False, u"Not a valid date"
elif self.python_type == 'ulines':
if isinstance(value, basestring):
try:
[unicode(x) for x in value.splitlines()]
except ValueError:
return False, u"Not a list of unicode strings"
elif value is not None:
value = Utils.flatten_lines(value)
try:
[unicode(x) for x in value]
except ValueError:
return False, u"Not a list of unicode strings"
elif self.python_type == 'lines':
if isinstance(value, basestring):
try:
[str(x) for x in value.splitlines()]
except ValueError:
return False, u"Not a list of strings"
elif value is not None:
value = Utils.flatten_lines(value)
try:
[str(x) for x in value]
except ValueError:
return False, u"Not a list of strings"
# check each TALES expression
for ve in self.getValidationExpressions():
ec = self._getExprContext(self, extra_namespaces=dict(value=value))
ex = Expression(ve.expression)
if not bool(ex(ec)):
return False, ve.message
# by default no validation expression made it invalid
return True, None
##
## Working with the persistent attributes
##
def getCoreAttribute(self, *key_and_default):
""" return the value of this attribute. If len(@key_and_default) = 2 is
the second one is a default value. If not don't fall back on a default.
"""
if not len(key_and_default) in (1,2):
raise ValueError, "Call getCoreAttribute(key [,default])"
if len(key_and_default) == 1:
return self.attributes[key_and_default[0]]
else:
return self.attributes.get(key_and_default[0], key_and_default[1])
security.declareProtected(VMS, 'getCoreAttributeKeys')
def getCoreAttributeKeys(self):
return list(self.attributes.keys())
security.declareProtected(VMS, 'getCoreAttributeKeyLabel')
def getCoreAttributeKeyLabel(self, key, html_ok=False):
""" return a string that explains what the key is.
The resturn string can contain HTML.
"""
if key == 'dom_id':
if html_ok:
return u'<abbr title="DOM element ID, not Zope object ID">DOM ID</abbr>'
else:
return u'DOM ID'
if key.startswith('on') and re.findall('on\w+', key):
return u'on' + key[2:].capitalize()
if key in ('rows','cols'):
return u'Textarea %s' % key
return key.title()
def getCoreAttributeKeySuggestions(self):
""" return a list of suggestions of attribute keys you might want to add """
suggestions = ['style','size', 'dom_id', 'onchange', 'onkeypress', 'onclick',
'onfocus', 'onblur', 'value',
]
# add more
if self.input_type == 'textarea':
suggestions.append('cols')
suggestions.append('rows')
elif self.input_type == 'select':
suggestions.append('multiple')
# reduce already used ones
suggestions = [x for x in suggestions if x not in self.attributes]
# sort them by their labels
suggestions = [(self.getCoreAttributeKeyLabel(x), x) for x in suggestions]
suggestions.sort()
# return just the keys
return [x[1] for x in suggestions]
def getCoreAttributeKeyName(self, key):
""" return what the suitable name for the key should be a input tag
"""
return u'%s:ustring' % key
def getDeleteableAttributeKeys(self):
""" return a list of keys of attributes you can delete """
all = Set(list(self.attributes.keys()))
not_ = Set(CORE_ATTRIBUTES)
return list(all - not_)
##
## Modifying the custom field
##
security.declareProtected(VMS, 'manage_saveFieldProperties')
def manage_saveFieldProperties(self, input_type=None, python_type=None,
title=None, mandatory=False,
extra_css=None, extra_js=None,
options=None, options_expression=None,
visibility_expression=None,
include_in_filter_options=False,
REQUEST=None,
**settings):
""" saving changes via the web """
if input_type is not None:
different = input_type != self.input_type
if not input_type in OK_input_types:
raise ValueError, "invalid input_type"
self.input_type = input_type
if different:
self._prepareByType()
if python_type is not None:
assert python_type in OK_python_types, "Invalid Python type (%r)" % python_type
self.python_type = python_type
if title is not None:
self.title = unicode(title)
self.mandatory = bool(mandatory)
self.include_in_filter_options = bool(include_in_filter_options)
if extra_css is not None:
self.extra_css = unicode(extra_css).strip()
if extra_js is not None:
self.extra_js = unicode(extra_js).strip()
if options_expression is not None:
self.options_expression = str(options_expression).strip()
if self.options_expression:
assert self._valid_options_expression(), "Invalid expression"
if visibility_expression is not None:
self.visibility_expression = visibility_expression
if options is not None:
self.options = flat_to_list(options)
if not settings and REQUEST is not None:
settings = self.REQUEST.form
# I don't like the pattern but it'll have to do for now
for key, value in settings.items():
if key not in CORE_ATTRIBUTES:
self.attributes[key] = value
if REQUEST is not None:
msg = 'Changes saved'
url = self.absolute_url()+'/manage_field'
url += '?manage_tabs_message=%s' % Utils.url_quote_plus(msg)
REQUEST.RESPONSE.redirect(url)
security.declareProtected(VMS, 'manage_addFieldProperty')
def manage_addFieldProperty(self, key=None, new_key=None, REQUEST=None):
""" add a new attribute property """
if not key and not new_key:
raise ValueError, "must pass 'key' OR 'new_key'"
if new_key:
key = new_key.strip()
key = str(key)
self.attributes[key] = u''
if REQUEST is not None:
msg = 'Field added'
url = self.absolute_url()+'/manage_field'
url += '?manage_tabs_message=%s' % Utils.url_quote_plus(msg)
url += '#field-%s' % key
REQUEST.RESPONSE.redirect(url)
security.declareProtected(VMS, 'manage_deleteFieldProperty')
def manage_deleteFieldProperty(self, key, REQUEST=None):
""" delete a field property """
del self.attributes[key]
if REQUEST is not None:
msg = 'Attribute deleted'
url = self.absolute_url()+'/manage_field'
url += '?manage_tabs_message=%s' % Utils.url_quote_plus(msg)
REQUEST.RESPONSE.redirect(url)
security.declareProtected(VMS, 'manage_addValidationExpression')
def manage_addValidationExpression(self, expression, message=u'', REQUEST=None):
""" add a new validation expression """
# check that it's not complete rubbish
expression = str(expression).strip()
message = unicode(message).strip()
if not expression:
raise ValueError, "Expression can't be empty"
# XXX: Got to figure out a better way to test the expression without a
# arbitrary value like this
## test it
#ec = self._getExprContext(self, extra_namespaces=dict(value='123'))
#ex = Expression(expression)
#try:
# ex(ec)
#except Exception, m:
# raise ValueError, m
c = len(self.objectIds(CUSTOMFIELD_VALIDATION_EXPRESSION_METATYPE)) + 1
oid = 'validation_%s' % c
while base_hasattr(self, oid):
c += 1
oid = 'validation_%s' % c
instance = ValidationExpression(oid, expression, message)
self._setObject(oid, instance)
if REQUEST is not None:
msg = 'Expression added'
url = self.absolute_url()+'/manage_validation'
url += '?manage_tabs_message=%s' % Utils.url_quote_plus(msg)
REQUEST.RESPONSE.redirect(url)
security.declareProtected(VMS, 'manage_deleteValidationExpression')
def manage_deleteValidationExpression(self, id, REQUEST=None):
""" delete a validation expression """
assert id in self.objectIds(CUSTOMFIELD_VALIDATION_EXPRESSION_METATYPE)
self.manage_delObjects([id])
if REQUEST is not None:
msg = 'Expression delete'
url = self.absolute_url()+'/manage_validation'
url += '?manage_tabs_message=%s' % Utils.url_quote_plus(msg)
REQUEST.RESPONSE.redirect(url)
security.declareProtected(VMS, 'manage_editValidationExpression')
def manage_editValidationExpression(self, id, expression, message,
delete=False, REQUEST=None):
""" change a validation expression object """
assert id in self.objectIds(CUSTOMFIELD_VALIDATION_EXPRESSION_METATYPE)
obj = getattr(self, id)
if delete:
return self.manage_deleteValidationExpression(id, REQUEST=REQUEST)
expression = str(expression).strip()
message = unicode(message).strip()
if not expression:
raise ValueError, "Expression can't be empty"
# test it
ec = self._getExprContext(self, extra_namespaces=dict(value='123'))
ex = Expression(expression)
try:
ex(ec)
except Exception, m:
raise ValueError, m
obj.expression = expression
obj.message = message
if REQUEST is not None:
msg = 'Expression changed'
url = self.absolute_url()+'/manage_validation'
url += '?manage_tabs_message=%s' % Utils.url_quote_plus(msg)
REQUEST.RESPONSE.redirect(url)
##
## Showing values of custom fields
##
def showValue(self, value):
""" return an HTML representation of a field for this value. """
if self.input_type in ('radio','checkbox'):
for option in self.getOptions():
if isinstance(option, (tuple, list)):
save_value, show_value = option
else:
save_value, show_value = option, option
if compare_custom_value(save_value, value, self.python_type):
return show_value
if self.python_type in ('lines','ulines') and isinstance(value, (tuple, list)):
return ', '.join(value)
elif self.input_type == 'password':
return '*' * max(1, len(value))
elif self.input_type == 'file':
return '<a href="%s">%s</a>' % (value, value.split('/')[-1])
#as_obj = self.restrictedTraverse(value)
return '<a href="%s">%s</a>' % (as_obj.absolute_url_path(), as_obj.getId())
elif self.input_type == 'select' and self.getOptionsIterable():
# try to show the label instead of the value
# if the list of options are tuples
for option in self.getOptionsIterable():
if isinstance(option, (tuple, list)):
value_, label = option
else:
value_, label = option, option
if compare_custom_value(value_, value, self.python_type):
return label
# default
return value
else:
return value
zpts = (
'zpt/customfield/manage_field',
'zpt/customfield/manage_validation',
'zpt/customfield/index_html',
)
addTemplates2Class(CustomField, zpts)
security = ClassSecurityInfo()
security.declareProtected(VMS, 'index_html')
security.declareProtected(VMS, 'manage_field')
security.declareProtected(VMS, 'manage_validation')
security.apply(CustomField)
InitializeClass(CustomField)
#----------------------------------------------------------------------------
from OFS.SimpleItem import SimpleItem
from OFS.PropertyManager import PropertyManager
class ValidationExpression(SimpleItem, PropertyManager):
""" a validation expression is a very simple object that consists of two
things: expression (str) and message (unicode)
"""
meta_type = CUSTOMFIELD_VALIDATION_EXPRESSION_METATYPE
def __init__(self, id, expression, message):
self.id = str(id)
self.expression = str(expression)
self.message = unicode(message)
#----------------------------------------------------------------------------
from IssueTracker import ZopeOrderedFolder
## https://bugs.launchpad.net/zope2/+bug/142399
def safe_hasattr(obj, name, _marker=object()):
"""Make sure we don't mask exceptions like hasattr().
We don't want exceptions other than AttributeError to be masked,
since that too often masks other programming errors.
Three-argument getattr() doesn't mask those, so we use that to
implement our own hasattr() replacement.
"""
return getattr(obj, name, _marker) is not _marker
def base_hasattr(obj, name):
"""Like safe_hasattr, but also disables acquisition."""
return safe_hasattr(aq_base(obj), name)
manage_addCustomFieldFolderForm = DTMLFile('dtml/addCustomFieldFolder', globals())
def manage_addCustomFieldFolder(self, oid='custom_fields',
title=u'Custom fields',
input_type='text',
REQUEST=None):
""" add a new CustomFieldFolder """
instance = CustomFieldFolder(oid, title=title)
self._setObject(oid, instance)
if REQUEST is not None:
REQUEST.RESPONSE.redirect(self.absolute_url()+'/manage_main')
else:
return self._getOb(oid)
class CustomFieldFolder(ZopeOrderedFolder):
""" A CustomFieldFolder is just a folder to put all the custom fields
but in order so that when rendering the Add Issue page we will know which
order to put them in. Plus
"""
meta_type = CUSTOMFIELDFOLDER_METATYPE
security = ClassSecurityInfo()
#icon = '%s/customfieldfolder.png' % ICON_LOCATION
def __init__(self, id, title=u'', extra_css='', extra_js=''):
self.id = str(id)
self.title = title
InitializeClass(CustomFieldFolder)
#----------------------------------------------------------------------------
class CustomFieldsIssueTrackerBase:
"""
This class is plugged into IssueTracker class so that the IssueTracker
can do batch operations over all findable custom field objects in places
like the.
"""
def _getExprContext(self, object, extra_namespaces={}):
return getExprContext(self, object, extra_namespaces=extra_namespaces)
def getCustomFieldObjects(self, field_ids_filter=None):
""" return an iterable list of all custom fields that we can reach.
The parameter @field_ids_filter can be a list, tuple or a function that
limits which fields should be returned.
"""
fields = []
folder = getattr(self, 'custom_fields', None)
if folder is not None and folder.meta_type == CUSTOMFIELDFOLDER_METATYPE:
fields.extend([x for x in folder.objectValues(CUSTOMFIELD_METATYPE)
if not x.disabled])
# add any found here
fields.extend([x for x in self.objectValues(CUSTOMFIELD_METATYPE)
if not x.disabled])
if isinstance(field_ids_filter, basestring):
field_ids_filter = [field_ids_filter]
if callable(field_ids_filter):
fields = [x for x in fields if field_ids_filter(x)]
elif field_ids_filter:
fields = [x for x in fields if x.getId() in field_ids_filter]
visible_fields = []
for field in fields:
if field.getVisibilityExpression():
# evaluate the expression
ec = self._getExprContext(self)
ex = Expression(field.getVisibilityExpression())
if not bool(ex(ec)):
continue
visible_fields.append(field)
if isinstance(field_ids_filter, (tuple, list)):
# respect the order
fields_dict = {}
for field in visible_fields:
fields_dict[field.getId()] = field
visible_fields = []
for each in field_ids_filter:
visible_fields.append(fields_dict[each])
return visible_fields
def getCustomFieldsCombinedCSS(self, field_ids_filter=None):
""" return a combined chunk of CSS for all custom fields """
chunks = []
for field in self.getCustomFieldObjects(field_ids_filter=field_ids_filter):
if field.extra_css:
chunks.append(field.render_extra_css())
return '\n'.join(chunks)
def manage_fix(self):
" legacy fixer "
for field in self.getCustomFieldObjects():
if not hasattr(field, 'include_in_filter_options'):
field.include_in_filter_options = False
if not hasattr(self, 'visibility_expression'):
self.visibility_expression = ''
return "done"
class CustomFieldsIssueBase:
""" Helping the IssueTrackerIssue class do batch operations on custom
fields.
"""
def setCustomFieldData(self, field, key, value):
""" append this to self.custom_fields_data (dict).
The parameter @field is the custom field object.
"""
if field.input_type == 'file':
# upload the file into the issue and change @value to the id
value.read(1)
if self._isFile(value):
# upload it!
folder_id = 'upload-%s' % field.getId()
if not safe_hasattr(self, folder_id):
self.manage_addFolder(folder_id)
container = getattr(self, folder_id)
ids = self._uploadFileattachments(container, [value])
ids = ['%s/%s' % (folder_id, x) for x in ids]
value = ids[0]
else:
# nothing worth saving
return
elif field.python_type == 'int':
value = int(value)
elif field.python_type == 'float':
value = float(value)
elif field.python_type == 'long':
value = long(value)
elif field.python_type == 'lines':
if isinstance(value, tuple):
value = list(value)
elif isinstance(value, basestring):
value = [value]
else:
# due to way Zope's cast handles <selects>
# with name "foo:ulines" you get
# ['one', ['two']]
value = Utils.flatten_lines(value)
assert isinstance(value, list), "value not a list"
# every item should be a str
value = [str(x) for x in value]
elif field.python_type == 'ulines':
if isinstance(value, tuple):
value = list(value)
elif isinstance(value, basestring):
value = [value]
elif value is None:
value = []
else:
# due to way Zope's cast handles <selects>
# with name "foo:ulines" you get
# ['one', ['two']]
if isinstance(value, list):
value = Utils.flatten_lines(value)
assert isinstance(value, list), "value not a list it's a %s" % type(value)
# every item should be a str
value = [unicodify(x) for x in value]
elif field.python_type == 'date':
if isinstance(value, basestring):
value = DateTime(value)
elif field.python_type == 'boolean':
value = bool(value)
elif field.python_type == 'ustring':
value = unicodify(value)
else:
value = str(value)
data = getattr(self, 'custom_fields_data', None)
if data is None:
self.custom_fields_data = PersistentMapping()
self.custom_fields_data[key] = value
def getCustomFieldsData(self, field_ids_filter=None, exclude_empty=False):
""" return a list of dict that contain the {field, key, value} """
values = {}
for key, value in getattr(self, 'custom_fields_data', {}).items():
values[key] = value
if not values:
return []
# return the key, value pairs together with the field in an ordered manner.
# This also makes sure we don't return data for which there is no field
fields = []
for field in self.getRoot().getCustomFieldObjects(field_ids_filter=field_ids_filter):
value = values.get(field.getId(), None)
if value is None:
continue
if exclude_empty:
# but if the field is a boolean python type include its "empty" value
if not value and field.getPythonType() != 'boolean':
continue
fields.append(dict(field=field, value=value, key=field.getId()))
return fields
def getCustomFieldData(self, key, default=None):
""" get custom field data """
data = getattr(self, 'custom_fields_data', {})
return data.get(key, default)
|