# ------------------------------------------------------------------------------ # 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 . # ------------------------------------------------------------------------------ import copy, types, re from appy import Object from appy.gen.layout import Table, defaultFieldLayouts from appy.gen import utils as gutils from appy.px import Px from appy.shared import utils as sutils from group import Group from page import Page # ------------------------------------------------------------------------------ class Field: '''Basic abstract class for defining any field.''' # Some global static variables nullValues = (None, '', []) validatorTypes = (types.FunctionType, types.UnboundMethodType, type(re.compile(''))) labelTypes = ('label', 'descr', 'help') # 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' # Render a field. Optiona vars: # * 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(''' :tool.pxLayoutedObject''') # Displays a field label. pxLabel = Px('''''') # Displays a field description. pxDescription = Px('''::zobj.translate('descr', field=field)''') # Displays a field help. pxHelp = Px('''''') # Displays validation-error-related info about a field. pxValidation = Px('''''') # Displays the fact that a field is required. pxRequired = Px('''''') # Button for showing changes to the field. pxChanges = Px('''''') 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: ": 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 or 1 # 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 = gutils.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 or 1 # 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 = 'read' 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 = 'write' 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 self.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 self.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) 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 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 self.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 '/onProcess.''' return obj.goto(obj.absolute_url()) # ------------------------------------------------------------------------------