876 lines
42 KiB
Python
876 lines
42 KiB
Python
# ------------------------------------------------------------------------------
|
|
# This file is part of Appy, a framework for building applications in the Python
|
|
# language. Copyright (C) 2007 Gaetan Delannay
|
|
|
|
# Appy is free software; you can redistribute it and/or modify it under the
|
|
# terms of the GNU General Public License as published by the Free Software
|
|
# Foundation; either version 3 of the License, or (at your option) any later
|
|
# version.
|
|
|
|
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# Appy. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
# ------------------------------------------------------------------------------
|
|
import copy, types, re
|
|
from appy.gen.layout import Table, defaultFieldLayouts
|
|
from appy.gen import utils as gutils
|
|
from appy.shared import utils as sutils
|
|
|
|
# ------------------------------------------------------------------------------
|
|
nullValues = (None, '', [])
|
|
validatorTypes = (types.FunctionType, types.UnboundMethodType,
|
|
type(re.compile('')))
|
|
labelTypes = ('label', 'descr', 'help')
|
|
|
|
def initMasterValue(v):
|
|
'''Standardizes p_v as a list of strings.'''
|
|
if not isinstance(v, bool) and not v: res = []
|
|
elif type(v) not in sutils.sequenceTypes: res = [v]
|
|
else: res = v
|
|
return [str(v) for v in res]
|
|
|
|
class No:
|
|
'''When you write a workflow condition method and you want to return False
|
|
but you want to give to the user some explanations about why a transition
|
|
can't be triggered, do not return False, return an instance of No
|
|
instead. When creating such an instance, you can specify an error
|
|
message.'''
|
|
def __init__(self, msg):
|
|
self.msg = msg
|
|
def __nonzero__(self):
|
|
return False
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Page. Every field lives into a Page.
|
|
# ------------------------------------------------------------------------------
|
|
class Page:
|
|
'''Used for describing a page, its related phase, show condition, etc.'''
|
|
subElements = ('save', 'cancel', 'previous', 'next', 'edit')
|
|
def __init__(self, name, phase='main', show=True, showSave=True,
|
|
showCancel=True, showPrevious=True, showNext=True,
|
|
showEdit=True):
|
|
self.name = name
|
|
self.phase = phase
|
|
self.show = show
|
|
# When editing the page, must I show the "save" button?
|
|
self.showSave = showSave
|
|
# When editing the page, must I show the "cancel" button?
|
|
self.showCancel = showCancel
|
|
# When editing the page, and when a previous page exists, must I show
|
|
# the "previous" button?
|
|
self.showPrevious = showPrevious
|
|
# When editing the page, and when a next page exists, must I show the
|
|
# "next" button?
|
|
self.showNext = showNext
|
|
# When viewing the page, must I show the "edit" button?
|
|
self.showEdit = showEdit
|
|
|
|
@staticmethod
|
|
def get(pageData):
|
|
'''Produces a Page instance from p_pageData. User-defined p_pageData
|
|
can be:
|
|
(a) a string containing the name of the page;
|
|
(b) a string containing <pageName>_<phaseName>;
|
|
(c) a Page instance.
|
|
This method returns always a Page instance.'''
|
|
res = pageData
|
|
if res and isinstance(res, basestring):
|
|
# Page data is given as a string.
|
|
pageElems = pageData.rsplit('_', 1)
|
|
if len(pageElems) == 1: # We have case (a)
|
|
res = Page(pageData)
|
|
else: # We have case (b)
|
|
res = Page(pageData[0], phase=pageData[1])
|
|
return res
|
|
|
|
def isShowable(self, obj, layoutType, elem='page'):
|
|
'''Must this page be shown for p_obj? "Show value" can be True, False
|
|
or 'view' (page is available only in "view" mode).
|
|
|
|
If p_elem is not "page", this method returns the fact that a
|
|
sub-element is viewable or not (buttons "save", "cancel", etc).'''
|
|
# Define what attribute to test for "showability".
|
|
showAttr = 'show'
|
|
if elem != 'page':
|
|
showAttr = 'show%s' % elem.capitalize()
|
|
# Get the value of the show attribute as identified above.
|
|
show = getattr(self, showAttr)
|
|
if callable(show):
|
|
show = show(obj.appy())
|
|
# Show value can be 'view', for example. Thanks to p_layoutType,
|
|
# convert show value to a real final boolean value.
|
|
res = show
|
|
if res == 'view': res = layoutType == 'view'
|
|
return res
|
|
|
|
def getInfo(self, obj, layoutType):
|
|
'''Gets information about this page, for p_obj, as a dict.'''
|
|
res = {}
|
|
for elem in Page.subElements:
|
|
res['show%s' % elem.capitalize()] = self.isShowable(obj, layoutType,
|
|
elem=elem)
|
|
return res
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Group. Fields can be grouped.
|
|
# ------------------------------------------------------------------------------
|
|
class Group:
|
|
'''Used for describing a group of widgets within a page.'''
|
|
def __init__(self, name, columns=['100%'], wide=True, style='section2',
|
|
hasLabel=True, hasDescr=False, hasHelp=False,
|
|
hasHeaders=False, group=None, colspan=1, align='center',
|
|
valign='top', css_class='', master=None, masterValue=None,
|
|
cellpadding=1, cellspacing=1, cellgap='0.6em', label=None,
|
|
translated=None):
|
|
self.name = name
|
|
# In its simpler form, field "columns" below can hold a list or tuple
|
|
# of column widths expressed as strings, that will be given as is in
|
|
# the "width" attributes of the corresponding "td" tags. Instead of
|
|
# strings, within this list or tuple, you may give Column instances
|
|
# (see below).
|
|
self.columns = columns
|
|
self._setColumns()
|
|
# If field "wide" below is True, the HTML table corresponding to this
|
|
# group will have width 100%. You can also specify some string value,
|
|
# which will be used for HTML param "width".
|
|
if wide == True:
|
|
self.wide = '100%'
|
|
elif isinstance(wide, basestring):
|
|
self.wide = wide
|
|
else:
|
|
self.wide = ''
|
|
# If style = 'fieldset', all widgets within the group will be rendered
|
|
# within an HTML fieldset. If style is 'section1' or 'section2', widgets
|
|
# will be rendered after the group title.
|
|
self.style = style
|
|
# If hasLabel is True, the group will have a name and the corresponding
|
|
# i18n label will be generated.
|
|
self.hasLabel = hasLabel
|
|
# If hasDescr is True, the group will have a description and the
|
|
# corresponding i18n label will be generated.
|
|
self.hasDescr = hasDescr
|
|
# If hasHelp is True, the group will have a help text associated and the
|
|
# corresponding i18n label will be generated.
|
|
self.hasHelp = hasHelp
|
|
# If hasheaders is True, group content will begin with a row of headers,
|
|
# and a i18n label will be generated for every header.
|
|
self.hasHeaders = hasHeaders
|
|
self.nbOfHeaders = len(columns)
|
|
# If this group is himself contained in another group, the following
|
|
# attribute is filled.
|
|
self.group = Group.get(group)
|
|
# If the group is rendered into another group, we can specify the number
|
|
# of columns that this group will span.
|
|
self.colspan = colspan
|
|
self.align = align
|
|
self.valign = valign
|
|
self.cellpadding = cellpadding
|
|
self.cellspacing = cellspacing
|
|
# Beyond standard cellpadding and cellspacing, cellgap can define an
|
|
# additional horizontal gap between cells in a row. So this value does
|
|
# not add space before the first cell or after the last one.
|
|
self.cellgap = cellgap
|
|
if style == 'tabs':
|
|
# Group content will be rendered as tabs. In this case, some
|
|
# param combinations have no sense.
|
|
self.hasLabel = self.hasDescr = self.hasHelp = False
|
|
# The rendering is forced to a single column
|
|
self.columns = self.columns[:1]
|
|
# Header labels will be used as labels for the tabs.
|
|
self.hasHeaders = True
|
|
self.css_class = css_class
|
|
self.master = master
|
|
self.masterValue = initMasterValue(masterValue)
|
|
if master: master.slaves.append(self)
|
|
self.label = label # See similar attr of Type class.
|
|
# If a translated name is already given here, we will use it instead of
|
|
# trying to translate the group label.
|
|
self.translated = translated
|
|
|
|
def _setColumns(self):
|
|
'''Standardizes field "columns" as a list of Column instances. Indeed,
|
|
the initial value for field "columns" may be a list or tuple of
|
|
Column instances or strings.'''
|
|
for i in range(len(self.columns)):
|
|
columnData = self.columns[i]
|
|
if not isinstance(columnData, Column):
|
|
self.columns[i] = Column(self.columns[i])
|
|
|
|
@staticmethod
|
|
def get(groupData):
|
|
'''Produces a Group instance from p_groupData. User-defined p_groupData
|
|
can be a string or a Group instance; this method returns always a
|
|
Group instance.'''
|
|
res = groupData
|
|
if res and isinstance(res, basestring):
|
|
# Group data is given as a string. 2 more possibilities:
|
|
# (a) groupData is simply the name of the group;
|
|
# (b) groupData is of the form <groupName>_<numberOfColumns>.
|
|
groupElems = groupData.rsplit('_', 1)
|
|
if len(groupElems) == 1:
|
|
res = Group(groupElems[0])
|
|
else:
|
|
try:
|
|
nbOfColumns = int(groupElems[1])
|
|
except ValueError:
|
|
nbOfColumns = 1
|
|
width = 100.0 / nbOfColumns
|
|
res = Group(groupElems[0], ['%.2f%%' % width] * nbOfColumns)
|
|
return res
|
|
|
|
def getMasterData(self):
|
|
'''Gets the master of this group (and masterValue) or, recursively, of
|
|
containing groups when relevant.'''
|
|
if self.master: return (self.master, self.masterValue)
|
|
if self.group: return self.group.getMasterData()
|
|
|
|
def generateLabels(self, messages, classDescr, walkedGroups,
|
|
forSearch=False):
|
|
'''This method allows to generate all the needed i18n labels related to
|
|
this group. p_messages is the list of i18n p_messages (a PoMessages
|
|
instance) that we are currently building; p_classDescr is the
|
|
descriptor of the class where this group is defined. If p_forSearch
|
|
is True, this group is used for grouping searches, and not fields.'''
|
|
# A part of the group label depends on p_forSearch.
|
|
if forSearch: gp = 'searchgroup'
|
|
else: gp = 'group'
|
|
if self.hasLabel:
|
|
msgId = '%s_%s_%s' % (classDescr.name, gp, self.name)
|
|
messages.append(msgId, self.name)
|
|
if self.hasDescr:
|
|
msgId = '%s_%s_%s_descr' % (classDescr.name, gp, self.name)
|
|
messages.append(msgId, ' ', nice=False)
|
|
if self.hasHelp:
|
|
msgId = '%s_%s_%s_help' % (classDescr.name, gp, self.name)
|
|
messages.append(msgId, ' ', nice=False)
|
|
if self.hasHeaders:
|
|
for i in range(self.nbOfHeaders):
|
|
msgId = '%s_%s_%s_col%d' % (classDescr.name, gp, self.name, i+1)
|
|
messages.append(msgId, ' ', nice=False)
|
|
walkedGroups.add(self)
|
|
if self.group and (self.group not in walkedGroups) and \
|
|
not self.group.label:
|
|
# We remember walked groups for avoiding infinite recursion.
|
|
self.group.generateLabels(messages, classDescr, walkedGroups,
|
|
forSearch=forSearch)
|
|
|
|
def insertInto(self, widgets, groupDescrs, page, metaType, forSearch=False):
|
|
'''Inserts the GroupDescr instance corresponding to this Group instance
|
|
into p_widgets, the recursive structure used for displaying all
|
|
widgets in a given p_page (or all searches), and returns this
|
|
GroupDescr instance.'''
|
|
# First, create the corresponding GroupDescr if not already in
|
|
# p_groupDescrs.
|
|
if self.name not in groupDescrs:
|
|
groupDescr = groupDescrs[self.name] = gutils.GroupDescr(\
|
|
self, page, metaType, forSearch=forSearch).get()
|
|
# Insert the group at the higher level (ie, directly in p_widgets)
|
|
# if the group is not itself in a group.
|
|
if not self.group:
|
|
widgets.append(groupDescr)
|
|
else:
|
|
outerGroupDescr = self.group.insertInto(widgets, groupDescrs,
|
|
page, metaType, forSearch=forSearch)
|
|
gutils.GroupDescr.addWidget(outerGroupDescr, groupDescr)
|
|
else:
|
|
groupDescr = groupDescrs[self.name]
|
|
return groupDescr
|
|
|
|
class Column:
|
|
'''Used for describing a column within a Group like defined above.'''
|
|
def __init__(self, width, align="left"):
|
|
self.width = width
|
|
self.align = align
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Abstract base class for every field.
|
|
# ------------------------------------------------------------------------------
|
|
class Field:
|
|
'''Basic abstract class for defining any field.'''
|
|
# Those attributes can be overridden by subclasses for defining,
|
|
# respectively, names of CSS and Javascript files that are required by this
|
|
# field, keyed by layoutType.
|
|
cssFiles = {}
|
|
jsFiles = {}
|
|
dLayouts = 'lrv-d-f'
|
|
|
|
def __init__(self, validator, multiplicity, default, show, page, group,
|
|
layouts, move, indexed, searchable, specificReadPermission,
|
|
specificWritePermission, width, height, maxChars, colspan,
|
|
master, masterValue, focus, historized, sync, mapping, label,
|
|
sdefault, scolspan, swidth, sheight):
|
|
# The validator restricts which values may be defined. It can be an
|
|
# interval (1,None), a list of string values ['choice1', 'choice2'],
|
|
# a regular expression, a custom function, a Selection instance, etc.
|
|
self.validator = validator
|
|
# Multiplicity is a 2-tuple indicating the minimum and maximum
|
|
# occurrences of values.
|
|
self.multiplicity = multiplicity
|
|
# Is the field required or not ? (derived from multiplicity)
|
|
self.required = self.multiplicity[0] > 0
|
|
# Default value
|
|
self.default = default
|
|
# Must the field be visible or not?
|
|
self.show = show
|
|
# When displaying/editing the whole object, on what page and phase must
|
|
# this field value appear?
|
|
self.page = Page.get(page)
|
|
self.pageName = self.page.name
|
|
# Within self.page, in what group of fields must this one appear?
|
|
self.group = Group.get(group)
|
|
# The following attribute allows to move a field back to a previous
|
|
# position (useful for moving fields above predefined ones).
|
|
self.move = move
|
|
# If indexed is True, a database index will be set on the field for
|
|
# fast access.
|
|
self.indexed = indexed
|
|
# If specified "searchable", the field will be added to some global
|
|
# index allowing to perform application-wide, keyword searches.
|
|
self.searchable = searchable
|
|
# Normally, permissions to read or write every attribute in a type are
|
|
# granted if the user has the global permission to read or
|
|
# edit instances of the whole type. If you want a given attribute
|
|
# to be protected by specific permissions, set one or the 2 next boolean
|
|
# values to "True". In this case, you will create a new "field-only"
|
|
# read and/or write permission. If you need to protect several fields
|
|
# with the same read/write permission, you can avoid defining one
|
|
# specific permission for every field by specifying a "named"
|
|
# permission (string) instead of assigning "True" to the following
|
|
# arg(s). A named permission will be global to your whole Zope site, so
|
|
# take care to the naming convention. Typically, a named permission is
|
|
# of the form: "<yourAppName>: Write|Read ---". If, for example, I want
|
|
# to define, for my application "MedicalFolder" a specific permission
|
|
# for a bunch of fields that can only be modified by a doctor, I can
|
|
# define a permission "MedicalFolder: Write medical information" and
|
|
# assign it to the "specificWritePermission" of every impacted field.
|
|
self.specificReadPermission = specificReadPermission
|
|
self.specificWritePermission = specificWritePermission
|
|
# Widget width and height
|
|
self.width = width
|
|
self.height = height
|
|
# While width and height refer to widget dimensions, maxChars hereafter
|
|
# represents the maximum number of chars that a given input field may
|
|
# accept (corresponds to HTML "maxlength" property). "None" means
|
|
# "unlimited".
|
|
self.maxChars = maxChars or ''
|
|
# If the widget is in a group with multiple columns, the following
|
|
# attribute specifies on how many columns to span the widget.
|
|
self.colspan = colspan
|
|
# The list of slaves of this field, if it is a master
|
|
self.slaves = []
|
|
# The behaviour of this field may depend on another, "master" field
|
|
self.master = master
|
|
if master: self.master.slaves.append(self)
|
|
# When master has some value(s), there is impact on this field.
|
|
self.masterValue = initMasterValue(masterValue)
|
|
# If a field must retain attention in a particular way, set focus=True.
|
|
# It will be rendered in a special way.
|
|
self.focus = focus
|
|
# If we must keep track of changes performed on a field, "historized"
|
|
# must be set to True.
|
|
self.historized = historized
|
|
# self.sync below determines if the field representations will be
|
|
# retrieved in a synchronous way by the browser or not (Ajax).
|
|
self.sync = self.formatSync(sync)
|
|
# Mapping is a dict of contexts that, if specified, are given when
|
|
# translating the label, descr or help related to this field.
|
|
self.mapping = self.formatMapping(mapping)
|
|
self.id = id(self)
|
|
self.type = self.__class__.__name__
|
|
self.pythonType = None # The True corresponding Python type
|
|
# Get the layouts. Consult layout.py for more info about layouts.
|
|
self.layouts = self.formatLayouts(layouts)
|
|
# Can we filter this field?
|
|
self.filterable = False
|
|
# Can this field have values that can be edited and validated?
|
|
self.validable = True
|
|
# The base label for translations is normally generated automatically.
|
|
# It is made of 2 parts: the prefix, based on class name, and the name,
|
|
# which is the field name by default. You can change this by specifying
|
|
# a value for param "label". If this value is a string, it will be
|
|
# understood as a new prefix. If it is a tuple, it will represent the
|
|
# prefix and another name. If you want to specify a new name only, and
|
|
# not a prefix, write (None, newName).
|
|
self.label = label
|
|
# When you specify a default value "for search" (= "sdefault"), on a
|
|
# search screen, in the search field corresponding to this field, this
|
|
# default value will be present.
|
|
self.sdefault = sdefault
|
|
# Colspan for rendering the search widget corresponding to this field.
|
|
self.scolspan = scolspan
|
|
# Width and height for the search widget
|
|
self.swidth = swidth or width
|
|
self.sheight = sheight or height
|
|
|
|
def init(self, name, klass, appName):
|
|
'''When the application server starts, this secondary constructor is
|
|
called for storing the name of the Appy field (p_name) and other
|
|
attributes that are based on the name of the Appy p_klass, and the
|
|
application name (p_appName).'''
|
|
if hasattr(self, 'name'): return # Already initialized
|
|
self.name = name
|
|
# Determine prefix for this class
|
|
if not klass: prefix = appName
|
|
else: prefix = gutils.getClassName(klass, appName)
|
|
# Recompute the ID (and derived attributes) that may have changed if
|
|
# we are in debug mode (because we recreate new Field instances).
|
|
self.id = id(self)
|
|
# Remember master name on every slave
|
|
for slave in self.slaves: slave.masterName = name
|
|
# Determine ids of i18n labels for this field
|
|
labelName = name
|
|
trPrefix = None
|
|
if self.label:
|
|
if isinstance(self.label, basestring): trPrefix = self.label
|
|
else: # It is a tuple (trPrefix, name)
|
|
if self.label[1]: labelName = self.label[1]
|
|
if self.label[0]: trPrefix = self.label[0]
|
|
if not trPrefix:
|
|
trPrefix = prefix
|
|
# Determine name to use for i18n
|
|
self.labelId = '%s_%s' % (trPrefix, labelName)
|
|
self.descrId = self.labelId + '_descr'
|
|
self.helpId = self.labelId + '_help'
|
|
# Determine read and write permissions for this field
|
|
rp = self.specificReadPermission
|
|
if rp and not isinstance(rp, basestring):
|
|
self.readPermission = '%s: Read %s %s' % (appName, prefix, name)
|
|
elif rp and isinstance(rp, basestring):
|
|
self.readPermission = rp
|
|
else:
|
|
self.readPermission = 'View'
|
|
wp = self.specificWritePermission
|
|
if wp and not isinstance(wp, basestring):
|
|
self.writePermission = '%s: Write %s %s' % (appName, prefix, name)
|
|
elif wp and isinstance(wp, basestring):
|
|
self.writePermission = wp
|
|
else:
|
|
self.writePermission = 'Modify portal content'
|
|
if (self.type == 'Ref') and not self.isBack:
|
|
# We must initialise the corresponding back reference
|
|
self.back.klass = klass
|
|
self.back.init(self.back.attribute, self.klass, appName)
|
|
if self.type == "List":
|
|
for subName, subField in self.fields:
|
|
fullName = '%s_%s' % (name, subName)
|
|
subField.init(fullName, klass, appName)
|
|
subField.name = '%s*%s' % (name, subName)
|
|
|
|
def reload(self, klass, obj):
|
|
'''In debug mode, we want to reload layouts without restarting Zope.
|
|
So this method will prepare a "new", reloaded version of p_self,
|
|
that corresponds to p_self after a "reload" of its containing Python
|
|
module has been performed.'''
|
|
res = getattr(klass, self.name, None)
|
|
if not res: return self
|
|
if (self.type == 'Ref') and self.isBack: return self
|
|
res.init(self.name, klass, obj.getProductConfig().PROJECTNAME)
|
|
return res
|
|
|
|
def isMultiValued(self):
|
|
'''Does this type definition allow to define multiple values?'''
|
|
res = False
|
|
maxOccurs = self.multiplicity[1]
|
|
if (maxOccurs == None) or (maxOccurs > 1):
|
|
res = True
|
|
return res
|
|
|
|
def isSortable(self, usage):
|
|
'''Can fields of this type be used for sorting purposes (when sorting
|
|
search results (p_usage="search") or when sorting reference fields
|
|
(p_usage="ref")?'''
|
|
if usage == 'search':
|
|
return self.indexed and not self.isMultiValued() and not \
|
|
((self.type == 'String') and self.isSelection())
|
|
elif usage == 'ref':
|
|
return self.type in ('Integer', 'Float', 'Boolean', 'Date') or \
|
|
((self.type == 'String') and (self.format == 0))
|
|
|
|
def isShowable(self, obj, layoutType):
|
|
'''When displaying p_obj on a given p_layoutType, must we show this
|
|
field?'''
|
|
# Check if the user has the permission to view or edit the field
|
|
if layoutType == 'edit': perm = self.writePermission
|
|
else: perm = self.readPermission
|
|
if not obj.allows(perm): return False
|
|
# Evaluate self.show
|
|
if callable(self.show):
|
|
res = self.callMethod(obj, self.show)
|
|
else:
|
|
res = self.show
|
|
# Take into account possible values 'view', 'edit', 'result'...
|
|
if type(res) in sutils.sequenceTypes:
|
|
for r in res:
|
|
if r == layoutType: return True
|
|
return False
|
|
elif res in ('view', 'edit', 'result'):
|
|
return res == layoutType
|
|
return bool(res)
|
|
|
|
def isClientVisible(self, obj):
|
|
'''This method returns True if this field is visible according to
|
|
master/slave relationships.'''
|
|
masterData = self.getMasterData()
|
|
if not masterData: return True
|
|
else:
|
|
master, masterValue = masterData
|
|
reqValue = master.getRequestValue(obj.REQUEST)
|
|
# reqValue can be a list or not
|
|
if type(reqValue) not in sutils.sequenceTypes:
|
|
return reqValue in masterValue
|
|
else:
|
|
for m in masterValue:
|
|
for r in reqValue:
|
|
if m == r: return True
|
|
|
|
def formatSync(self, sync):
|
|
'''Creates a dictionary indicating, for every layout type, if the field
|
|
value must be retrieved synchronously or not.'''
|
|
if isinstance(sync, bool):
|
|
sync = {'edit': sync, 'view': sync, 'cell': sync}
|
|
for layoutType in ('edit', 'view', 'cell'):
|
|
if layoutType not in sync:
|
|
sync[layoutType] = False
|
|
return sync
|
|
|
|
def formatMapping(self, mapping):
|
|
'''Creates a dict of mappings, one entry by label type (label, descr,
|
|
help).'''
|
|
if isinstance(mapping, dict):
|
|
# Is it a dict like {'label':..., 'descr':...}, or is it directly a
|
|
# dict with a mapping?
|
|
for k, v in mapping.iteritems():
|
|
if (k not in labelTypes) or isinstance(v, basestring):
|
|
# It is already a mapping
|
|
return {'label':mapping, 'descr':mapping, 'help':mapping}
|
|
# If we are here, we have {'label':..., 'descr':...}. Complete
|
|
# it if necessary.
|
|
for labelType in labelTypes:
|
|
if labelType not in mapping:
|
|
mapping[labelType] = None # No mapping for this value.
|
|
return mapping
|
|
else:
|
|
# Mapping is a method that must be applied to any i18n message.
|
|
return {'label':mapping, 'descr':mapping, 'help':mapping}
|
|
|
|
def formatLayouts(self, layouts):
|
|
'''Standardizes the given p_layouts. .'''
|
|
# First, get the layouts as a dictionary, if p_layouts is None or
|
|
# expressed as a simple string.
|
|
areDefault = False
|
|
if not layouts:
|
|
# Get the default layouts as defined by the subclass
|
|
areDefault = True
|
|
layouts = self.computeDefaultLayouts()
|
|
else:
|
|
if isinstance(layouts, basestring):
|
|
# The user specified a single layoutString (the "edit" one)
|
|
layouts = {'edit': layouts}
|
|
elif isinstance(layouts, Table):
|
|
# Idem, but with a Table instance
|
|
layouts = {'edit': Table(other=layouts)}
|
|
else:
|
|
# Here, we make a copy of the layouts, because every layout can
|
|
# be different, even if the user decides to reuse one from one
|
|
# field to another. This is because we modify every layout for
|
|
# adding master/slave-related info, focus-related info, etc,
|
|
# which can be different from one field to the other.
|
|
layouts = copy.deepcopy(layouts)
|
|
if 'edit' not in layouts:
|
|
defEditLayout = self.computeDefaultLayouts()
|
|
if type(defEditLayout) == dict:
|
|
defEditLayout = defEditLayout['edit']
|
|
layouts['edit'] = defEditLayout
|
|
# We have now a dict of layouts in p_layouts. Ensure now that a Table
|
|
# instance is created for every layout (=value from the dict). Indeed,
|
|
# a layout could have been expressed as a simple layout string.
|
|
for layoutType in layouts.iterkeys():
|
|
if isinstance(layouts[layoutType], basestring):
|
|
layouts[layoutType] = Table(layouts[layoutType])
|
|
# Derive "view" and "cell" layouts from the "edit" layout when relevant
|
|
if 'view' not in layouts:
|
|
layouts['view'] = Table(other=layouts['edit'], derivedType='view')
|
|
# Create the "cell" layout from the 'view' layout if not specified.
|
|
if 'cell' not in layouts:
|
|
layouts['cell'] = Table(other=layouts['view'], derivedType='cell')
|
|
# Put the required CSS classes in the layouts
|
|
layouts['cell'].addCssClasses('noStyle')
|
|
if self.focus:
|
|
# We need to make it flashy
|
|
layouts['view'].addCssClasses('focus')
|
|
layouts['edit'].addCssClasses('focus')
|
|
# If layouts are the default ones, set width=None instead of width=100%
|
|
# for the field if it is not in a group (excepted for rich texts).
|
|
if areDefault and not self.group and \
|
|
not ((self.type == 'String') and (self.format == self.XHTML)):
|
|
for layoutType in layouts.iterkeys():
|
|
layouts[layoutType].width = ''
|
|
# Remove letters "r" from the layouts if the field is not required.
|
|
if not self.required:
|
|
for layoutType in layouts.iterkeys():
|
|
layouts[layoutType].removeElement('r')
|
|
# Derive some boolean values from the layouts.
|
|
self.hasLabel = self.hasLayoutElement('l', layouts)
|
|
self.hasDescr = self.hasLayoutElement('d', layouts)
|
|
self.hasHelp = self.hasLayoutElement('h', layouts)
|
|
# Store Table instance's dicts instead of instances: this way, they can
|
|
# be manipulated in ZPTs.
|
|
for layoutType in layouts.iterkeys():
|
|
layouts[layoutType] = layouts[layoutType].get()
|
|
return layouts
|
|
|
|
def hasLayoutElement(self, element, layouts):
|
|
'''This method returns True if the given layout p_element can be found
|
|
at least once among the various p_layouts defined for this field.'''
|
|
for layout in layouts.itervalues():
|
|
if element in layout.layoutString: return True
|
|
return False
|
|
|
|
def getDefaultLayouts(self):
|
|
'''Any subclass can define this for getting a specific set of
|
|
default layouts. If None is returned, a global set of default layouts
|
|
will be used.'''
|
|
|
|
def getInputLayouts(self):
|
|
'''Gets, as a string, the layouts as could have been specified as input
|
|
value for the Field constructor.'''
|
|
res = '{'
|
|
for k, v in self.layouts.iteritems():
|
|
res += '"%s":"%s",' % (k, v['layoutString'])
|
|
res += '}'
|
|
return res
|
|
|
|
def computeDefaultLayouts(self):
|
|
'''This method gets the default layouts from an Appy type, or a copy
|
|
from the global default field layouts when they are not available.'''
|
|
res = self.getDefaultLayouts()
|
|
if not res:
|
|
# Get the global default layouts
|
|
res = copy.deepcopy(defaultFieldLayouts)
|
|
return res
|
|
|
|
def getCss(self, layoutType, res):
|
|
'''This method completes the list p_res with the names of CSS files
|
|
that are required for displaying widgets of self's type on a given
|
|
p_layoutType. p_res is not a set because order of inclusion of CSS
|
|
files may be important and may be loosed by using sets.'''
|
|
if layoutType in self.cssFiles:
|
|
for fileName in self.cssFiles[layoutType]:
|
|
if fileName not in res:
|
|
res.append(fileName)
|
|
|
|
def getJs(self, layoutType, res):
|
|
'''This method completes the list p_res with the names of Javascript
|
|
files that are required for displaying widgets of self's type on a
|
|
given p_layoutType. p_res is not a set because order of inclusion of
|
|
CSS files may be important and may be loosed by using sets.'''
|
|
if layoutType in self.jsFiles:
|
|
for fileName in self.jsFiles[layoutType]:
|
|
if fileName not in res:
|
|
res.append(fileName)
|
|
|
|
def getValue(self, obj):
|
|
'''Gets, on_obj, the value conforming to self's type definition.'''
|
|
value = getattr(obj.aq_base, self.name, None)
|
|
if self.isEmptyValue(value):
|
|
# If there is no value, get the default value if any: return
|
|
# self.default, of self.default() if it is a method.
|
|
if callable(self.default):
|
|
try:
|
|
return self.callMethod(obj, self.default)
|
|
except Exception, e:
|
|
# Already logged. Here I do not raise the exception,
|
|
# because it can be raised as the result of reindexing
|
|
# the object in situations that are not foreseen by
|
|
# method in self.default.
|
|
return None
|
|
else:
|
|
return self.default
|
|
return value
|
|
|
|
def getFormattedValue(self, obj, value, showChanges=False):
|
|
'''p_value is a real p_obj(ect) value from a field from this type. This
|
|
method returns a pretty, string-formatted version, for displaying
|
|
purposes. Needs to be overridden by some child classes. If
|
|
p_showChanges is True, the result must also include the changes that
|
|
occurred on p_value across the ages.'''
|
|
if self.isEmptyValue(value): return ''
|
|
return value
|
|
|
|
def getIndexType(self):
|
|
'''Returns the name of the technical, Zope-level index type for this
|
|
field.'''
|
|
# Normally, self.indexed contains a Boolean. If a string value is given,
|
|
# we consider it to be an index type. It allows to bypass the standard
|
|
# way to decide what index type must be used.
|
|
if isinstance(self.indexed, str): return self.indexed
|
|
if self.name == 'title': return 'TextIndex'
|
|
return 'FieldIndex'
|
|
|
|
def getIndexValue(self, obj, forSearch=False):
|
|
'''This method returns a version for this field value on p_obj that is
|
|
ready for indexing purposes. Needs to be overridden by some child
|
|
classes.
|
|
|
|
If p_forSearch is True, it will return a "string" version of the
|
|
index value suitable for a global search.'''
|
|
value = self.getValue(obj)
|
|
if forSearch and (value != None):
|
|
if isinstance(value, unicode):
|
|
res = value.encode('utf-8')
|
|
elif type(value) in sutils.sequenceTypes:
|
|
res = []
|
|
for v in value:
|
|
if isinstance(v, unicode): res.append(v.encode('utf-8'))
|
|
else: res.append(str(v))
|
|
res = ' '.join(res)
|
|
else:
|
|
res = str(value)
|
|
return res
|
|
return value
|
|
|
|
def getRequestValue(self, request, requestName=None):
|
|
'''Gets a value for this field as carried in the request object. In the
|
|
simplest cases, the request value is a single value whose name in the
|
|
request is the name of the field.
|
|
|
|
Sometimes (ie: a Date: see the overriden method in the Date class),
|
|
several request values must be combined.
|
|
|
|
Sometimes (ie, a field which is a sub-field in a List), the name of
|
|
the request value(s) representing the field value do not correspond
|
|
to the field name (ie: the request name includes information about
|
|
the container field). In this case, p_requestName must be used for
|
|
searching into the request, instead of the field name (self.name).'''
|
|
name = requestName or self.name
|
|
return request.get(name, None)
|
|
|
|
def getStorableValue(self, value):
|
|
'''p_value is a valid value initially computed through calling
|
|
m_getRequestValue. So, it is a valid string (or list of strings)
|
|
representation of the field value coming from the request.
|
|
This method computes the real (potentially converted or manipulated
|
|
in some other way) value as can be stored in the database.'''
|
|
if self.isEmptyValue(value): return None
|
|
return value
|
|
|
|
def getMasterData(self):
|
|
'''Gets the master of this field (and masterValue) or, recursively, of
|
|
containing groups when relevant.'''
|
|
if self.master: return (self.master, self.masterValue)
|
|
if self.group: return self.group.getMasterData()
|
|
|
|
def isEmptyValue(self, value, obj=None):
|
|
'''Returns True if the p_value must be considered as an empty value.'''
|
|
return value in nullValues
|
|
|
|
def validateValue(self, obj, value):
|
|
'''This method may be overridden by child classes and will be called at
|
|
the right moment by m_validate defined below for triggering
|
|
type-specific validation. p_value is never empty.'''
|
|
return None
|
|
|
|
def securityCheck(self, obj, value):
|
|
'''This method performs some security checks on the p_value that
|
|
represents user input.'''
|
|
if not isinstance(value, basestring): return
|
|
# Search Javascript code in the value (prevent XSS attacks).
|
|
if '<script' in value:
|
|
obj.log('Detected Javascript in user input.', type='error')
|
|
raise Exception('Your behaviour is considered a security ' \
|
|
'attack. System administrator has been warned.')
|
|
|
|
def validate(self, obj, value):
|
|
'''This method checks that p_value, coming from the request (p_obj is
|
|
being created or edited) and formatted through a call to
|
|
m_getRequestValue defined above, is valid according to this type
|
|
definition. If it is the case, None is returned. Else, a translated
|
|
error message is returned.'''
|
|
# Check that a value is given if required.
|
|
if self.isEmptyValue(value, obj):
|
|
if self.required and self.isClientVisible(obj):
|
|
# If the field is required, but not visible according to
|
|
# master/slave relationships, we consider it not to be required.
|
|
return obj.translate('field_required')
|
|
else:
|
|
return None
|
|
# Perform security checks on p_value
|
|
self.securityCheck(obj, value)
|
|
# Triggers the sub-class-specific validation for this value
|
|
message = self.validateValue(obj, value)
|
|
if message: return message
|
|
# Evaluate the custom validator if one has been specified
|
|
value = self.getStorableValue(value)
|
|
if self.validator and (type(self.validator) in validatorTypes):
|
|
obj = obj.appy()
|
|
if type(self.validator) != validatorTypes[-1]:
|
|
# It is a custom function. Execute it.
|
|
try:
|
|
validValue = self.validator(obj, value)
|
|
if isinstance(validValue, basestring) and validValue:
|
|
# Validation failed; and p_validValue contains an error
|
|
# message.
|
|
return validValue
|
|
else:
|
|
if not validValue:
|
|
return obj.translate('field_invalid')
|
|
except Exception, e:
|
|
return str(e)
|
|
except:
|
|
return obj.translate('field_invalid')
|
|
else:
|
|
# It is a regular expression
|
|
if not self.validator.match(value):
|
|
# If the regular expression is among the default ones, we
|
|
# generate a specific error message.
|
|
if self.validator == String.EMAIL:
|
|
return obj.translate('bad_email')
|
|
elif self.validator == String.URL:
|
|
return obj.translate('bad_url')
|
|
elif self.validator == String.ALPHANUMERIC:
|
|
return obj.translate('bad_alphanumeric')
|
|
else:
|
|
return obj.translate('field_invalid')
|
|
|
|
def store(self, obj, value):
|
|
'''Stores the p_value (produced by m_getStorableValue) that complies to
|
|
p_self type definition on p_obj.'''
|
|
setattr(obj, self.name, value)
|
|
|
|
def callMethod(self, obj, method, cache=True):
|
|
'''This method is used to call a p_method on p_obj. p_method is part of
|
|
this type definition (ie a default method, the method of a Computed
|
|
field, a method used for showing or not a field...). Normally, those
|
|
methods are called without any arg. But one may need, within the
|
|
method, to access the related field. This method tries to call
|
|
p_method with no arg *or* with the field arg.'''
|
|
obj = obj.appy()
|
|
try:
|
|
return gutils.callMethod(obj, method, cache=cache)
|
|
except TypeError, te:
|
|
# Try a version of the method that would accept self as an
|
|
# additional parameter. In this case, we do not try to cache the
|
|
# value (we do not call gutils.callMethod), because the value may
|
|
# be different depending on the parameter.
|
|
tb = sutils.Traceback.get()
|
|
try:
|
|
return method(obj, self)
|
|
except Exception, e:
|
|
obj.log(tb, type='error')
|
|
# Raise the initial error.
|
|
raise te
|
|
except Exception, e:
|
|
obj.log(sutils.Traceback.get(), type='error')
|
|
raise e
|
|
|
|
def process(self, obj):
|
|
'''This method is a general hook allowing a field to perform some
|
|
processing after an URL on an object has been called, of the form
|
|
<objUrl>/onProcess.'''
|
|
return obj.goto(obj.absolute_url())
|
|
# ------------------------------------------------------------------------------
|