2013-07-08 16:39:16 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# 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
|
2013-08-21 06:54:56 -05:00
|
|
|
from appy import Object
|
2013-07-08 16:39:16 -05:00
|
|
|
from appy.gen.layout import Table, defaultFieldLayouts
|
|
|
|
from appy.gen import utils as gutils
|
2013-08-21 05:35:30 -05:00
|
|
|
from appy.px import Px
|
2013-08-21 06:54:56 -05:00
|
|
|
from appy.shared import utils as sutils
|
|
|
|
from group import Group
|
|
|
|
from page import Page
|
2013-08-21 05:35:30 -05:00
|
|
|
|
2013-07-08 16:39:16 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
class Field:
|
|
|
|
'''Basic abstract class for defining any field.'''
|
2013-08-21 06:54:56 -05:00
|
|
|
|
|
|
|
# Some global static variables
|
|
|
|
nullValues = (None, '', [])
|
|
|
|
validatorTypes = (types.FunctionType, types.UnboundMethodType,
|
|
|
|
type(re.compile('')))
|
|
|
|
labelTypes = ('label', 'descr', 'help')
|
|
|
|
|
2013-07-08 16:39:16 -05:00
|
|
|
# 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 = {}
|
2014-08-05 02:48:05 -05:00
|
|
|
bLayouts = Table('lrv-f', width=None)
|
2013-07-08 16:39:16 -05:00
|
|
|
dLayouts = 'lrv-d-f'
|
2014-04-30 14:08:42 -05:00
|
|
|
hLayouts = 'lhrv-f'
|
2014-02-26 03:40:27 -06:00
|
|
|
wLayouts = Table('lrv-f')
|
2013-07-08 16:39:16 -05:00
|
|
|
|
2014-02-27 05:54:05 -06:00
|
|
|
# Render a field. Optional vars:
|
2013-08-21 05:35:30 -05:00
|
|
|
# * fieldName can be given as different as field.name for fields included
|
|
|
|
# in List fields: in this case, fieldName includes the row
|
|
|
|
# index.
|
|
|
|
# * showChanges If True, a variant of the field showing successive changes
|
|
|
|
# made to it is shown.
|
|
|
|
pxRender = Px('''
|
2014-03-03 11:54:21 -06:00
|
|
|
<x var="showChanges=showChanges|req.get('showChanges',False);
|
|
|
|
layoutType=layoutType|req.get('layoutType');
|
2014-03-05 06:25:36 -06:00
|
|
|
isSearch = layoutType == 'search';
|
2013-08-21 05:35:30 -05:00
|
|
|
layout=field.layouts[layoutType];
|
|
|
|
name=fieldName|field.name;
|
2014-03-05 06:25:36 -06:00
|
|
|
widgetName = isSearch and ('w_%s' % name) or name;
|
2013-08-21 05:35:30 -05:00
|
|
|
outerValue=value|None;
|
2014-03-05 06:25:36 -06:00
|
|
|
rawValue=not isSearch and zobj.getFieldValue(name, \
|
2014-03-05 09:19:11 -06:00
|
|
|
layoutType=layoutType, outerValue=outerValue);
|
2014-03-05 06:25:36 -06:00
|
|
|
value=not isSearch and \
|
|
|
|
field.getFormattedValue(zobj, rawValue, showChanges);
|
|
|
|
requestValue=not isSearch and zobj.getRequestFieldValue(name);
|
2013-08-21 05:35:30 -05:00
|
|
|
inRequest=req.has_key(name);
|
2014-03-04 08:03:37 -06:00
|
|
|
error=req.get('%s_error' % name);
|
2013-08-21 05:35:30 -05:00
|
|
|
isMultiple=(field.multiplicity[1] == None) or \
|
|
|
|
(field.multiplicity[1] > 1);
|
|
|
|
masterCss=field.slaves and ('master_%s' % name) or '';
|
2014-03-03 11:54:21 -06:00
|
|
|
slaveCss=field.getSlaveCss();
|
2013-08-21 05:35:30 -05:00
|
|
|
tagCss=tagCss|'';
|
|
|
|
tagCss=('%s %s' % (slaveCss, tagCss)).strip();
|
2014-03-05 06:25:36 -06:00
|
|
|
zobj=zobj or ztool;
|
2014-04-19 12:58:02 -05:00
|
|
|
tagId='%s_%s' % (zobj.id, name);
|
2013-08-21 05:35:30 -05:00
|
|
|
tagName=field.master and 'slave' or '';
|
|
|
|
layoutTarget=field">:tool.pxLayoutedObject</x>''')
|
|
|
|
|
|
|
|
# Displays a field label.
|
2014-04-30 08:10:11 -05:00
|
|
|
pxLabel = Px('''<label if="field.hasLabel and field.renderLabel"
|
|
|
|
lfor=":field.name">::_('label', field=field)</label>''')
|
2013-08-21 05:35:30 -05:00
|
|
|
|
|
|
|
# Displays a field description.
|
|
|
|
pxDescription = Px('''<span if="field.hasDescr"
|
2014-04-30 08:10:11 -05:00
|
|
|
class="discreet">::_('descr', field=field)</span>''')
|
2013-08-21 05:35:30 -05:00
|
|
|
|
|
|
|
# Displays a field help.
|
2014-04-30 08:10:11 -05:00
|
|
|
pxHelp = Px('''<acronym title=":_('help', field=field)"><img
|
2013-08-21 05:35:30 -05:00
|
|
|
src=":url('help')"/></acronym>''')
|
|
|
|
|
|
|
|
# Displays validation-error-related info about a field.
|
2014-03-04 08:03:37 -06:00
|
|
|
pxValidation = Px('''<x><acronym if="error" title=":error"><img
|
|
|
|
src=":url('warning')"/></acronym><img if="not error"
|
2013-08-21 05:35:30 -05:00
|
|
|
src=":url('warning_no.gif')"/></x>''')
|
|
|
|
|
|
|
|
# Displays the fact that a field is required.
|
|
|
|
pxRequired = Px('''<img src=":url('required.gif')"/>''')
|
|
|
|
|
|
|
|
# Button for showing changes to the field.
|
|
|
|
pxChanges = Px('''<x if=":zobj.hasHistory(name)"><img class="clickable"
|
|
|
|
if="not showChanges" src=":url('changes')" title="_('changes_show')"
|
2014-08-05 02:48:05 -05:00
|
|
|
onclick=":'askField(%s,%s,%s,null,%s)' % \
|
2013-08-21 05:35:30 -05:00
|
|
|
(q(tagId), q(zobj.absolute_url()), q('view'), q('True'))"/><img
|
|
|
|
class="clickable" if="showChanges" src=":url('changesNo')"
|
2014-08-05 02:48:05 -05:00
|
|
|
onclick=":'askField(%s,%s,%s,null,%s)' % \
|
2013-08-21 05:35:30 -05:00
|
|
|
(q(tagId), q(zobj.absolute_url(), q('view'), q('True'))"
|
|
|
|
title=":_('changes_hide')"/></x>''')
|
|
|
|
|
2013-07-08 16:39:16 -05:00
|
|
|
def __init__(self, validator, multiplicity, default, show, page, group,
|
|
|
|
layouts, move, indexed, searchable, specificReadPermission,
|
|
|
|
specificWritePermission, width, height, maxChars, colspan,
|
2014-03-05 09:19:11 -06:00
|
|
|
master, masterValue, focus, historized, mapping, label,
|
2014-03-04 08:03:37 -06:00
|
|
|
sdefault, scolspan, swidth, sheight, persist):
|
2013-07-08 16:39:16 -05:00
|
|
|
# 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".
|
2013-07-10 02:56:35 -05:00
|
|
|
self.maxChars = maxChars or ''
|
2013-07-08 16:39:16 -05:00
|
|
|
# If the widget is in a group with multiple columns, the following
|
|
|
|
# attribute specifies on how many columns to span the widget.
|
2013-08-21 05:35:30 -05:00
|
|
|
self.colspan = colspan or 1
|
2013-07-08 16:39:16 -05:00
|
|
|
# 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)
|
2014-03-03 11:54:21 -06:00
|
|
|
# The semantics of attribute "masterValue" below is as follows:
|
|
|
|
# - if "masterValue" is anything but a method, the field will be shown
|
|
|
|
# only when the master has this value, or one of it if multivalued;
|
|
|
|
# - if "masterValue" is a method, the value(s) of the slave field will
|
|
|
|
# be returned by this method, depending on the master value(s) that
|
|
|
|
# are given to it, as its unique parameter.
|
2013-08-21 06:54:56 -05:00
|
|
|
self.masterValue = gutils.initMasterValue(masterValue)
|
2013-07-08 16:39:16 -05:00
|
|
|
# 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
|
|
|
|
# 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.
|
2013-08-21 05:35:30 -05:00
|
|
|
self.scolspan = scolspan or 1
|
2013-07-08 16:39:16 -05:00
|
|
|
# Width and height for the search widget
|
|
|
|
self.swidth = swidth or width
|
|
|
|
self.sheight = sheight or height
|
2014-03-04 08:03:37 -06:00
|
|
|
# "persist" indicates if field content must be stored in the database.
|
|
|
|
# For some fields it is not wanted (ie, fields used only as masters to
|
|
|
|
# update slave's selectable values).
|
|
|
|
self.persist = persist
|
2013-07-08 16:39:16 -05:00
|
|
|
|
|
|
|
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:
|
2013-08-23 11:57:27 -05:00
|
|
|
self.readPermission = 'read'
|
2013-07-08 16:39:16 -05:00
|
|
|
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:
|
2013-08-23 11:57:27 -05:00
|
|
|
self.writePermission = 'write'
|
2013-07-08 16:39:16 -05:00
|
|
|
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?'''
|
|
|
|
maxOccurs = self.multiplicity[1]
|
2014-07-25 08:07:31 -05:00
|
|
|
return (maxOccurs == None) or (maxOccurs > 1)
|
2013-07-08 16:39:16 -05:00
|
|
|
|
|
|
|
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
|
2014-05-03 15:45:51 -05:00
|
|
|
perm = (layoutType == 'edit') and self.writePermission or \
|
|
|
|
self.readPermission
|
|
|
|
if not obj.allows(perm): return
|
2013-07-08 16:39:16 -05:00
|
|
|
# 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
|
2014-05-03 15:45:51 -05:00
|
|
|
return
|
2013-07-08 16:39:16 -05:00
|
|
|
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
|
2014-03-03 11:54:21 -06:00
|
|
|
if masterValue and callable(masterValue): return True
|
2013-07-08 16:39:16 -05:00
|
|
|
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 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():
|
2013-08-21 06:54:56 -05:00
|
|
|
if (k not in self.labelTypes) or isinstance(v, basestring):
|
2013-07-08 16:39:16 -05:00
|
|
|
# It is already a mapping
|
|
|
|
return {'label':mapping, 'descr':mapping, 'help':mapping}
|
|
|
|
# If we are here, we have {'label':..., 'descr':...}. Complete
|
|
|
|
# it if necessary.
|
2013-08-21 06:54:56 -05:00
|
|
|
for labelType in self.labelTypes:
|
2013-07-08 16:39:16 -05:00
|
|
|
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])
|
2014-03-05 06:25:36 -06:00
|
|
|
# Derive "view", "search" and "cell" layouts from the "edit" layout
|
|
|
|
# when relevant.
|
2013-07-08 16:39:16 -05:00
|
|
|
if 'view' not in layouts:
|
|
|
|
layouts['view'] = Table(other=layouts['edit'], derivedType='view')
|
2014-03-05 06:25:36 -06:00
|
|
|
if 'search' not in layouts:
|
|
|
|
layouts['search'] = Table(other=layouts['view'],
|
|
|
|
derivedType='search')
|
2013-07-08 16:39:16 -05:00
|
|
|
# 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%
|
2014-02-27 05:54:05 -06:00
|
|
|
# for the field if it is not in a group (excepted for rich texts and
|
|
|
|
# refs).
|
2013-07-08 16:39:16 -05:00
|
|
|
if areDefault and not self.group and \
|
2014-02-27 05:54:05 -06:00
|
|
|
not ((self.type == 'String') and (self.format == self.XHTML)) and \
|
|
|
|
not (self.type == 'Ref'):
|
2013-07-08 16:39:16 -05:00
|
|
|
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)
|
2014-04-30 08:10:11 -05:00
|
|
|
# "renderLabel" indicates if the existing label (if hasLabel is True)
|
|
|
|
# must be rendered by pxLabel. For example, if field is an action, the
|
|
|
|
# label will be rendered within the button, not by pxLabel.
|
|
|
|
self.renderLabel = self.hasLabel
|
|
|
|
# If field is within a group rendered like a tab, the label will already
|
|
|
|
# be rendered in the corresponding tab.
|
|
|
|
if self.group and (self.group.style == 'tabs'): self.renderLabel = False
|
2013-07-08 16:39:16 -05:00
|
|
|
self.hasDescr = self.hasLayoutElement('d', layouts)
|
|
|
|
self.hasHelp = self.hasLayoutElement('h', layouts)
|
|
|
|
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():
|
2013-08-21 15:25:27 -05:00
|
|
|
res += '"%s":"%s",' % (k, v.layoutString)
|
2013-07-08 16:39:16 -05:00
|
|
|
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.
|
2013-08-08 05:00:33 -05:00
|
|
|
return
|
2013-07-08 16:39:16 -05:00
|
|
|
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.'''
|
2013-09-20 10:42:07 -05:00
|
|
|
if self.isEmptyValue(value): return
|
2013-07-08 16:39:16 -05:00
|
|
|
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()
|
|
|
|
|
2014-03-03 11:54:21 -06:00
|
|
|
def getSlaveCss(self):
|
|
|
|
'''Gets the CSS class that must apply to this field in the web UI when
|
|
|
|
this field is the slave of another field.'''
|
|
|
|
if not self.master: return ''
|
|
|
|
res = 'slave*%s*' % self.masterName
|
|
|
|
if not callable(self.masterValue):
|
|
|
|
res += '*'.join(self.masterValue)
|
|
|
|
else:
|
|
|
|
res += '+'
|
|
|
|
return res
|
|
|
|
|
2014-03-05 06:25:36 -06:00
|
|
|
def getOnChange(self, zobj, layoutType, className=None):
|
2014-03-03 11:54:21 -06:00
|
|
|
'''When this field is a master, this method computes the call to the
|
|
|
|
Javascript function that will be called when its value changes (in
|
|
|
|
order to update slaves).'''
|
|
|
|
if not self.slaves: return ''
|
|
|
|
q = zobj.getTool().quote
|
2014-03-05 06:25:36 -06:00
|
|
|
# When the field is on a search screen, we need p_className.
|
|
|
|
cName = className and (',%s' % q(className)) or ''
|
|
|
|
return 'updateSlaves(this,null,%s,%s,null,null%s)' % \
|
|
|
|
(q(zobj.absolute_url()), q(layoutType), cName)
|
2014-03-03 11:54:21 -06:00
|
|
|
|
2013-07-08 16:39:16 -05:00
|
|
|
def isEmptyValue(self, value, obj=None):
|
|
|
|
'''Returns True if the p_value must be considered as an empty value.'''
|
2013-08-21 06:54:56 -05:00
|
|
|
return value in self.nullValues
|
2013-07-08 16:39:16 -05:00
|
|
|
|
|
|
|
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.'''
|
2013-09-20 10:42:07 -05:00
|
|
|
return
|
2013-07-08 16:39:16 -05:00
|
|
|
|
|
|
|
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:
|
2014-03-25 09:11:21 -05:00
|
|
|
return
|
2013-07-08 16:39:16 -05:00
|
|
|
# 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)
|
2013-08-21 06:54:56 -05:00
|
|
|
if self.validator and (type(self.validator) in self.validatorTypes):
|
2013-07-08 16:39:16 -05:00
|
|
|
obj = obj.appy()
|
2013-08-21 06:54:56 -05:00
|
|
|
if type(self.validator) != self.validatorTypes[-1]:
|
2013-07-08 16:39:16 -05:00
|
|
|
# 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):
|
2014-03-25 09:11:21 -05:00
|
|
|
return obj.translate('field_invalid')
|
2013-07-08 16:39:16 -05:00
|
|
|
|
|
|
|
def store(self, obj, value):
|
|
|
|
'''Stores the p_value (produced by m_getStorableValue) that complies to
|
|
|
|
p_self type definition on p_obj.'''
|
2014-03-04 08:03:37 -06:00
|
|
|
if self.persist: setattr(obj, self.name, value)
|
2013-07-08 16:39:16 -05:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2014-04-05 02:49:54 -05:00
|
|
|
def getAttribute(self, obj, name):
|
|
|
|
'''Gets the value of attribue p_name on p_self, which can be a simple
|
|
|
|
value or the result of a method call on p_obj.'''
|
|
|
|
res = getattr(self, name)
|
|
|
|
if not callable(res): return res
|
|
|
|
return self.callMethod(obj, res)
|
|
|
|
|
2013-07-08 16:39:16 -05:00
|
|
|
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())
|
|
|
|
# ------------------------------------------------------------------------------
|