[gen] Moved Appy fields into appy/fields together with their PX.

This commit is contained in:
Gaetan Delannay 2013-07-08 23:39:16 +02:00
parent 2b5d286668
commit 25b4edfc1d
24 changed files with 3795 additions and 2503 deletions

875
fields/__init__.py Normal file
View file

@ -0,0 +1,875 @@
# ------------------------------------------------------------------------------
# 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
# 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())
# ------------------------------------------------------------------------------

114
fields/action.py Normal file
View file

@ -0,0 +1,114 @@
# ------------------------------------------------------------------------------
# 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/>.
# ------------------------------------------------------------------------------
from appy.fields import Field
from appy.px import Px
from appy.shared import utils as sutils
# ------------------------------------------------------------------------------
class Action(Field):
'''An action is a Python method that can be triggered by the user on a
given gen-class. An action is rendered as a button.'''
# PX for viewing the Action button.
pxView = pxCell = Px('''
<form name="executeAppyAction"
var="formId='%s_%s_form' % (contextObj.UID(), name);
label=_(widget['labelId'])"
id=":formId" action=":ztool.absolute_url() + '/do'">
<input type="hidden" name="action" value="ExecuteAppyAction"/>
<input type="hidden" name="objectUid" value=":contextObj.UID()"/>
<input type="hidden" name="fieldName" value=":name"/>
<x if="widget['confirm']"><input
type="button" class="button"
var="labelConfirm=_(widget['labelId'] + '_confirm')"
value=":ztool.truncateValue(label)" title=":label"
style=":'background-image: url(%s/ui/buttonAction.png)' % appUrl"
onclick=":'askConfirm(%s,%s,%s)' % (q('form'), q(formId), \
q(labelConfirm))"/>
</x>
<input if="not widget['confirm']" type="submit" class="button" name="do"
value=":ztool.truncateValue(label)" title=":label"
style=":'background-image: url(%s/ui/buttonAction.png)' % appUrl"/>
</form>''')
# It is not possible to edit an action, not to search it.
pxEdit = pxSearch = ''
def __init__(self, validator=None, multiplicity=(1,1), default=None,
show=True, page='main', group=None, layouts=None, move=0,
indexed=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, action=None, result='computation',
confirm=False, master=None, masterValue=None, focus=False,
historized=False, mapping=None, label=None):
# Can be a single method or a list/tuple of methods
self.action = action
# For the 'result' param:
# * value 'computation' means that the action will simply compute
# things and redirect the user to the same page, with some status
# message about execution of the action;
# * 'file' means that the result is the binary content of a file that
# the user will download.
# * 'filetmp' is similar to file, but the file is a temp file and Appy
# will delete it as soon as it will be served to the browser.
# * 'redirect' means that the action will lead to the user being
# redirected to some other page.
self.result = result
# If following field "confirm" is True, a popup will ask the user if
# she is really sure about triggering this action.
self.confirm = confirm
Field.__init__(self, None, (0,1), default, show, page, group, layouts,
move, indexed, False, specificReadPermission,
specificWritePermission, width, height, None, colspan,
master, masterValue, focus, historized, False, mapping,
label, None, None, None, None)
self.validable = False
def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'}
def __call__(self, obj):
'''Calls the action on p_obj.'''
if type(self.action) in sutils.sequenceTypes:
# There are multiple Python methods
res = [True, '']
for act in self.action:
actRes = act(obj)
if type(actRes) in sutils.sequenceTypes:
res[0] = res[0] and actRes[0]
if self.result.startswith('file'):
res[1] = res[1] + actRes[1]
else:
res[1] = res[1] + '\n' + actRes[1]
else:
res[0] = res[0] and actRes
else:
# There is only one Python method
actRes = self.action(obj)
if type(actRes) in sutils.sequenceTypes:
res = list(actRes)
else:
res = [actRes, '']
# If res is None (ie the user-defined action did not return anything),
# we consider the action as successfull.
if res[0] == None: res[0] = True
return res
def isShowable(self, obj, layoutType):
if layoutType == 'edit': return False
else: return Field.isShowable(self, obj, layoutType)
# ------------------------------------------------------------------------------

99
fields/boolean.py Normal file
View file

@ -0,0 +1,99 @@
# ------------------------------------------------------------------------------
# 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/>.
# ------------------------------------------------------------------------------
from appy.fields import Field
from appy.px import Px
from appy.gen.layout import Table
# ------------------------------------------------------------------------------
class Boolean(Field):
'''Field for storing boolean values.'''
pxView = pxCell = Px('''
<x><x>:value</x>
<input type="hidden" if="masterCss"
class=":masterCss" value=":rawValue" name=":name" id=":name"/>
</x>''')
pxEdit = Px('''
<x var="isChecked=contextObj.checkboxChecked(name, rawValue)">
<input type="checkbox" name=":name + '_visible'" id=":name"
class=":masterCss" checked=":isChecked"
onclick=":'toggleCheckbox(%s, %s); updateSlaves(this)' % \
(q(name), q('%s_hidden' % name))"/>
<input type="hidden" name=":name" id=":'%s_hidden' % name"
value=":isChecked and 'True' or 'False')"/>
</x>''')
pxSearch = Px('''
<x var="typedWidget='%s*bool' % widgetName">
<label lfor=":widgetName">:_(widget['labelId'])"></label><br/>&nbsp;&nbsp;
<x var="valueId='%s_yes' % name">
<input type="radio" value="True" name=":typedWidget" id=":valueId"/>
<label lfor=":valueId">:_('yes')"></label>
</x>
<x var="valueId='%s_no' % name">
<input type="radio" value="False" name=":typedWidget" id=":valueId"/>
<label lfor=":valueId">:_('no')"></label>
</x>
<x var="valueId='%s_whatever' % name">
<input type="radio" value="" name=":typedWidget" id=":valueId"
checked="checked"/>
<label lfor=":valueId">:_('whatever')"></label>
</x><br/>
</x>''')
def __init__(self, validator=None, multiplicity=(0,1), default=None,
show=True, page='main', group=None, layouts = None, move=0,
indexed=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, label=None,
sdefault=False, scolspan=1, swidth=None, sheight=None):
Field.__init__(self, validator, multiplicity, default, show, page,
group, layouts, move, indexed, searchable,
specificReadPermission, specificWritePermission, width,
height, None, colspan, master, masterValue, focus,
historized, True, mapping, label, sdefault, scolspan,
swidth, sheight)
self.pythonType = bool
# Layout including a description
dLayouts = {'view': 'lf', 'edit': Table('flrv;=d', width=None)}
# Centered layout, no description
cLayouts = {'view': 'lf|', 'edit': 'flrv|'}
def getDefaultLayouts(self):
return {'view': 'lf', 'edit': Table('f;lrv;=', width=None)}
def getValue(self, obj):
'''Never returns "None". Returns always "True" or "False", even if
"None" is stored in the DB.'''
value = Field.getValue(self, obj)
if value == None: return False
return value
def getFormattedValue(self, obj, value, showChanges=False):
if value: res = obj.translate('yes')
else: res = obj.translate('no')
return res
def getStorableValue(self, value):
if not self.isEmptyValue(value):
exec 'res = %s' % value
return res
# ------------------------------------------------------------------------------

View file

@ -1,17 +1,250 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import types import types
from appy import Object from appy import Object
from appy.gen import Type from appy.gen import Field
from appy.px import Px
from DateTime import DateTime from DateTime import DateTime
from BTrees.IOBTree import IOBTree from BTrees.IOBTree import IOBTree
from persistent.list import PersistentList from persistent.list import PersistentList
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class Calendar(Type): class Calendar(Field):
'''This field allows to produce an agenda (monthly view) and view/edit '''This field allows to produce an agenda (monthly view) and view/edit
events on it.''' events on it.'''
jsFiles = {'view': ('widgets/calendar.js',)} jsFiles = {'view': ('widgets/calendar.js',)}
# Month view for a calendar. Called by pxView, and directly from the UI,
# via Ajax, when the user selects another month.
pxMonthView = Px('''
<div var="fieldName=req['fieldName'];
ajaxHookId=contextObj.UID() + fieldName;
month=req['month'];
monthDayOne=DateTime('%s/01' % month);
today=DateTime('00:00');
grid=contextObj.callField(fieldName, 'getMonthGrid', month);
allEventTypes=contextObj.callField(fieldName, 'getEventTypes', \
contextObj);
preComputed=contextObj.callField(fieldName, \
'getPreComputedInfo', contextObj, monthDayOne, grid);
defaultDate=contextObj.callField(fieldName, 'getDefaultDate', \
contextObj);
defaultDateMonth=defaultDate.strftime('%Y/%m');
previousMonth=contextObj.callField(fieldName, \
'getSiblingMonth', month, 'previous');
nextMonth=contextObj.callField(fieldName, 'getSiblingMonth', \
month, 'next');
widget=contextObj.getAppyType(fieldName, asDict=True);
mayEdit=contextObj.allows(widget['writePermission']);
objUrl=contextObj/absolute_url();
startDate=contextObj.callField(fieldName, 'getStartDate', \
contextObj);
endDate=contextObj.callField(fieldName, 'getEndDate',contextObj);
otherCalendars=contextObj.callField(fieldName, \
'getOtherCalendars', contextObj, preComputed)"
id=":ajaxHookId">
<script type="text/javascript">:'var %s_maxEventLength = %d;' % \
(fieldName, widget['maxEventLength'])">
</script>
<!-- Month chooser -->
<div style="margin-bottom: 5px"
var="fmt='%Y/%m/%d';
goBack=not startDate or (startDate.strftime(fmt) &lt; \
grid[0][0].strftime(fmt));
goForward=not endDate or (endDate.strftime(fmt) &gt; \
grid[-1][-1].strftime(fmt))">
<!-- Go to the previous month -->
<img style="cursor:pointer" tal:condition="goBack"
src=":'%s/ui/arrowLeftSimple.png' % appUrl"
onclick=":'askMonthView(%s, %s, %s, %s)' % \
(q(ajaxHookId),q(objUrl),q(fieldName),q(previousMonth))"/>
<!-- Go back to the default date -->
<x if="goBack or goForward">
<input type="button"
var="fmt='%Y/%m';
label=(defaultDate.strftime(fmt)==today.strftime(fmt)) and \
'today' or 'goto_source'"
value=":_(label)"
onclick=":'askMonthView(%s, %s, %s, %s)' % (q(ajaxHookId), \
q(objUrl), q(fieldName), q(defaultDateMonth))"
disabled=":defaultDate.strftime(fmt)==monthDayOne.strftime(fmt)"/>
</x>
<!-- Go to the next month -->
<img style="cursor:pointer" if="goForward"
src=":'%s/ui/arrowRightSimple.png' % appUrl"
onclick=":'askMonthView(%s, %s, %s, %s)' % (q(ajaxHookId), \
q(objUrl), q(fieldName), q(nextMonth))"/>
<span>:_('month_%s' % monthDayOne.aMonth())</span>
<span>:month.split('/')[0]</span>
</div>
<!-- Calendar month view -->
<table cellpadding="0" cellspacing="0" width="100%" class="list"
style="font-size: 95%"
var="rowHeight=int(widget['height']/float(len(grid)))">
<!-- 1st row: names of days -->
<tr height="22px">
<th for="dayName in contextObj.callField(fieldName, 'getNamesOfDays', \
contextObj)"
width="14%">:dayName</th>
</tr>
<!-- The calendar in itself -->
<tr for="row in grid" valign="top" height=":rowHeight">
<x for="date in row">
<x var="tooEarly=startDate and (date &lt; startDate);
tooLate=endDate and not tooEarly and (date &gt; endDate);
inRange=not tooEarly and not tooLate;
cssClasses=contextObj.callField(fieldName, 'getCellStyle', \
contextObj, date, today)">
<!-- Dump an empty cell if we are out of the supported date range -->
<td if="not inRange" class=":cssClasses"></td>
<!-- Dump a normal cell if we are in range -->
<x if="inRange">
<td var="events=contextObj.callField(fieldName, 'getEventsAt', \
contextObj, date);
spansDays=contextObj.callField(fieldName, 'hasEventsAt', \
contextObj, date+1, events);
mayCreate=mayEdit and not events;
mayDelete=mayEdit and events"
style="date.isCurrentDay() and 'font-weight:bold' or \
'font-weight:normal'"
class=":cssClasses"
onmouseover=":mayEdit and 'this.getElementsByTagName(\
%s)[0].style.visibility=%s' % (q('img'), q('visible')) or ''"
onmouseout="mayEdit and 'this.getElementsByTagName(\
%s)[0].style.visibility=%s' % (q('img'), q('hidden')) or ''">
<x var="day=date.day();
dayString=date.strftime('%Y/%m/%d')">
<span>:day</span>
<span if="day == 1">:_('month_%s_short' % date.aMonth())"></span>
<!-- Icon for adding an event -->
<x if="mayCreate">
<img style="visibility:hidden; cursor:pointer"
var="info=contextObj.callField(fieldName, \
'getApplicableEventsTypesAt', contextObj, date, \
allEventTypes, preComputed, True)"
if="info['eventTypes']"
src=":'%s/ui/plus.png' % appUrl"
onclick=":'openEventPopup(%s, %s, %s, null, %s, %s)' % \
(q('new'), q(fieldName), q(dayString), \
q(info['eventTypes']), q(info['message']))"/>
</x>
<!-- Icon for deleting an event -->
<img if="mayDelete" style="visibility:hidden; cursor:pointer"
src=":'%s/ui/delete.png' % appUrl"
onclick=":'openEventPopup(%s, %s, %s, %s, null, null)' % \
(q('del'), q(fieldName), q(dayString), q(str(spansDays)))"/>
<x if="events">
<!-- A single event is allowed for the moment -->
<div var="eventType=events[0]['eventType']">
<span style="color: grey">:contextObj.callField(fieldName, \
'getEventName', contextObj, eventType)"></span>
</div>
</x>
<!-- Events from other calendars -->
<x if="otherCalendars">
<x var="otherEvents=contextObj.callField(fieldName, \
'getOtherEventsAt', contextObj, date, otherCalendars)"
if="otherEvents">
<div style=":'color: %s; font-style: italic' % event['color']"
for="event in otherEvents">:event['name']</div>
</x>
</x>
<!-- Additional info -->
<x var="info=contextObj.callField(fieldName, \
'getAdditionalInfoAt', contextObj, date, preComputed)"
if="info">::info</x>
</x>
</td>
</x>
</x>
</x>
</tr>
</table>
<!-- Popup for creating a calendar event -->
<div var="prefix='%s_newEvent' % fieldName;
popupId=prefix + 'Popup'"
id=":popupId" class="popup" align="center">
<form id="prefix + 'Form'" method="post">
<input type="hidden" name="fieldName" value=":fieldName"/>
<input type="hidden" name="month" value=":month"/>
<input type="hidden" name="name" value=":fieldName"/>
<input type="hidden" name="action" value="Process"/>
<input type="hidden" name="actionType" value="createEvent"/>
<input type="hidden" name="day"/>
<!-- Choose an event type -->
<div align="center" style="margin-bottom: 3px">:_('which_event')"></div>
<select name="eventType">
<option value="">:_('choose_a_value')"></option>
<option for="eventType in allEventTypes"
value=":eventType">:contextObj.callField(fieldName, \
'getEventName', contextObj, eventType)">
</option>
</select><br/><br/>
<!--Span the event on several days -->
<div align="center" class="discreet" style="margin-bottom: 3px">
<span>:_('event_span')"></span>
<input type="text" size="3" name="eventSpan"/>
</div>
<input type="button"
value=":_('object_save')"
onclick=":'triggerCalendarEvent(%s, %s, %s, %s, \
%s_maxEventLength)' % (q('new'), q(ajaxHookId), \
q(fieldName), q(objUrl), fieldName)"/>
<input type="button"
value=":_('object_cancel')"
onclick=":'closePopup(%s)' % q(popupId)"/>
</form>
</div>
<!-- Popup for deleting a calendar event -->
<div var="prefix='%s_delEvent' % fieldName;
popupId=prefix + 'Popup'"
id=":popupId" class="popup" align="center">
<form id=":prefix + 'Form'" method="post">
<input type="hidden" name="fieldName" value=":fieldName"/>
<input type="hidden" name="month" value=":month"/>
<input type="hidden" name="name" value=":fieldName"/>
<input type="hidden" name="action" value="Process"/>
<input type="hidden" name="actionType" value="deleteEvent"/>
<input type="hidden" name="day"/>
<div align="center" style="margin-bottom: 5px">_('delete_confirm')">
</div>
<!-- Delete successive events ? -->
<div class="discreet" style="margin-bottom: 10px"
id=":prefix + 'DelNextEvent'">
<input type="checkbox" name="deleteNext_cb"
id=":prefix + '_cb'"
onClick=":'toggleCheckbox(%s, %s)' % \
(q('%s_cb' % prefix), q('%s_hd' % prefix))"/>
<input type="hidden" id=":prefix + '_hd'" name="deleteNext"/>
<span>:_('del_next_events')"></span>
</div>
<input type="button" value=":_('yes')"
onClick=":'triggerCalendarEvent(%s, %s, %s, %s)' % \
(q('del'), q(ajaxHookId), q(fieldName), q(objUrl))"/>
<input type="button" value=":_('no')"
onclick=":'closePopup(%s)' % q(popupId)"/>
</form>
</div>
</div>''')
pxView = pxCell = Px('''
<x var="defaultDate=contextObj.callField(widget['name'], 'getDefaultDate',\
contextObj);
x=req.set('fieldName', widget['name']);
x=req.set('month', defaultDate.strftime('%Y/%m'))">
<x>:widget['pxMonthView']></x>
</x>
''')
pxEdit = pxSearch = ''
def __init__(self, eventTypes, eventNameMethod=None, validator=None, def __init__(self, eventTypes, eventNameMethod=None, validator=None,
default=None, show='view', page='main', group=None, default=None, show='view', page='main', group=None,
layouts=None, move=0, specificReadPermission=False, layouts=None, move=0, specificReadPermission=False,
@ -21,11 +254,11 @@ class Calendar(Type):
otherCalendars=None, additionalInfo=None, startDate=None, otherCalendars=None, additionalInfo=None, startDate=None,
endDate=None, defaultDate=None, preCompute=None, endDate=None, defaultDate=None, preCompute=None,
applicableEvents=None): applicableEvents=None):
Type.__init__(self, validator, (0,1), default, show, page, group, Field.__init__(self, validator, (0,1), default, show, page, group,
layouts, move, False, False, specificReadPermission, layouts, move, False, False, specificReadPermission,
specificWritePermission, width, height, None, colspan, specificWritePermission, width, height, None, colspan,
master, masterValue, focus, False, True, mapping, label, master, masterValue, focus, False, True, mapping, label,
None, None, None, None) None, None, None, None)
# eventTypes can be a "static" list or tuple of strings that identify # eventTypes can be a "static" list or tuple of strings that identify
# the types of events that are supported by this calendar. It can also # the types of events that are supported by this calendar. It can also
# be a method that computes such a "dynamic" list or tuple. When # be a method that computes such a "dynamic" list or tuple. When

118
fields/computed.py Normal file
View file

@ -0,0 +1,118 @@
# ------------------------------------------------------------------------------
# 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/>.
# ------------------------------------------------------------------------------
from appy.fields import Field
from appy.px import Px
# ------------------------------------------------------------------------------
class Computed(Field):
# Ajax-called view content of a non sync Computed field.
pxViewContent = Px('''
<x var="name=req['fieldName'];
widget=contextObj.getAppyType(name, asDict=True);
value=contextObj.getFieldValue(name);
sync=True">:widget['pxView']</x>''')
pxView = pxCell = pxEdit = Px('''
<x>
<x if="sync">
<x if="widget['plainText']">:value</x>
<x if="not widget['plainText']">::value></x>
</x>
<x if="not sync">
<div var="ajaxHookId=contextObj.UID() + name" id="ajaxHookId">
<script type="text/javascript">:'askComputedField(%s, %s, %s)' % \
(q(ajaxHookId), q(contextObj.absolute_url()), q(name))">
</script>
</div>
</x>
</x>''')
pxSearch = Px('''
<x>
<label lfor=":widgetName">:_(widget['labelId'])"></label><br/>&nbsp;&nbsp;
<input type="text"
var="maxChars=widget['maxChars'] and widget['maxChars'] or ''"
name=":'%s*string' % widgetName"
maxlength=":maxChars" size=":widget['width']"
value=":widget['sdefault']"/>
</x>''')
def __init__(self, validator=None, multiplicity=(0,1), default=None,
show='view', page='main', group=None, layouts=None, move=0,
indexed=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, method=None, plainText=True,
master=None, masterValue=None, focus=False, historized=False,
sync=True, mapping=None, label=None, sdefault='', scolspan=1,
swidth=None, sheight=None, context={}):
# The Python method used for computing the field value
self.method = method
# Does field computation produce plain text or XHTML?
self.plainText = plainText
if isinstance(method, basestring):
# When field computation is done with a macro, we know the result
# will be HTML.
self.plainText = False
# The context is a dict (or method returning a dict) that will be given
# to the macro specified in self.method. If the dict contains key
# "someKey", it will be available to the macro as "options/someKey".
self.context = context
Field.__init__(self, None, multiplicity, default, show, page, group,
layouts, move, indexed, searchable,
specificReadPermission, specificWritePermission, width,
height, None, colspan, master, masterValue, focus,
historized, sync, mapping, label, sdefault, scolspan,
swidth, sheight)
self.validable = False
def callMacro(self, obj, macroPath):
'''Returns the macro corresponding to p_macroPath. The base folder
where we search is "ui".'''
# Get the special page in Appy that allows to call a macro
macroPage = obj.ui.callMacro
# Get, from p_macroPath, the page where the macro lies, and the macro
# name.
names = self.method.split('/')
# Get the page where the macro lies
page = obj.ui
for name in names[:-1]:
page = getattr(page, name)
macroName = names[-1]
# Compute the macro context.
ctx = {'contextObj':obj, 'page':page, 'macroName':macroName}
if callable(self.context):
ctx.update(self.context(obj.appy()))
else:
ctx.update(self.context)
return macroPage(obj, **ctx)
def getValue(self, obj):
'''Computes the value instead of getting it in the database.'''
if not self.method: return
if isinstance(self.method, basestring):
# self.method is a path to a macro that will produce the field value
return self.callMacro(obj, self.method)
else:
# self.method is a method that will return the field value
return self.callMethod(obj, self.method, cache=False)
def getFormattedValue(self, obj, value, showChanges=False):
if not isinstance(value, basestring): return str(value)
return value
# ------------------------------------------------------------------------------

251
fields/date.py Normal file
View file

@ -0,0 +1,251 @@
# ------------------------------------------------------------------------------
# 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 time
from appy.fields import Field
from appy.px import Px
# ------------------------------------------------------------------------------
class Date(Field):
pxView = pxCell = Px('''<x>:value</x>''')
pxEdit = Px('''
<x var="years=contextObj.getSelectableYears(widget['name'])">
<!-- Day -->
<select var="days=range(1,32)"
name=":'%s_day' % name" id=":'%s_day' % name">
<option value="">-</option>
<x for="day in days">
<option var="zDay=str(day).zfill(2)" value=":zDay"
selected="contextObj.dateValueSelected(name, 'day', day, \
rawValue)">:zDay></option></x>
</select>
<!-- Month -->
<select var="months=range(1,13)"
name=":'%s_month' % name" id=":'%s_month' % name">
<option value="">-</option>
<x for="month in months">
<option var="zMonth=str(month).zfill(2)" value=":zMonth"
selected="contextObj.dateValueSelected(name, 'month', month, \
rawValue)">:zMonth</option></x>
</select>
<!-- Year -->
<select name=":'%s_year' % name" id=":'%s_year' % name">
<option value="">-</option>
<option for="year in years" value=":year"
selected="contextObj.dateValueSelected(name, 'year', year, \
rawValue)">:year</option>
</select>
<!-- The icon for displaying the calendar popup -->
<x if="widget['calendar']">
<input type="hidden" id=":name" name=":name"/>
<img id=":'%s_img' % name" src=":'%s/ui/calendar.gif' % appUrl"/>
<script type="text/javascript">:contextObj.getCalendarInit(name, years)
</script>
</x>
<!-- Hour and minutes -->
<x if="widget['format'] == 0">
<select var="hours=range(0,24)"
name=":'%s_hour' % name" id=":'%s_hour' % name">
<option value="">-</option>
<x for="hour in hours">
<option var="zHour=str(hour).zfill(2)" value=":zHour"
selected=":contextObj.dateValueSelected(name, 'hour', hour, \
rawValue)">:zHour</option></x>
</select> :
<select var="minutes=range(0,60,5)"
name=":'%s_minute' % name" id=":'%s_minute' % name">
<option value="">-</option>
<x for="minute in minutes">
<option var="zMinute=str(minute).zfill(2)" value=":zMinute"
selected=":contextObj.dateValueSelected(name, 'minute', \
minute, rawValue)">:zMinute</option></x>
</select>
</x>
</x>''')
pxSearch = Px('''
<x var="years=range(widget['startYear'], widget['endYear']+1)">
<label>:_(widget['labelId'])</label>
<table>
<!-- From -->
<tr var="fromName='%s_from' % name;
dayFromName='%s_from_day' % name;
monthFromName='%s_from_month' % name;
yearFromName='%s*date' % widgetName">
<td width="10px">&nbsp;</td>
<td><label>:_('search_from')"></label></td>
<td>
<select id=":dayFromName" name=":dayFromName">
<option value="">--</option>
<option for="value in [str(v).zfill(2) for v in range(1, 32)]"
value=":value">:value</option>
</select> /
<select id=":monthFromName" name=":monthFromName">
<option value="">--</option>
<option for="value in [str(v).zfill(2) for v in range(1, 13)]"
value=":value">:value</option>
</select> /
<select id=":yearFromName" name=":yearFromName">
<option value="">--</option>
<option for="value in range(widget['startYear'],widget['endYear']+1)"
value=":value">:value</option>
</select>
<!-- The icon for displaying the calendar popup -->
<x if="widget['calendar']">
<input type="hidden" id=":fromName" name=":fromName"/>
<img id=":'%s_img' % fromName" src=":'%s/ui/calendar.gif' % appUrl"/>
<script type="text/javascript">:tool.getCalendarInit(fromName, years)
</script>
</x>
</td>
</tr>
<!-- To -->
<tr var="toName='%s_to' % name;
dayToName='%s_to_day' % name;
monthToName='%s_to_month' % name;
yearToName='%s_to_year' % name">
<td></td>
<td><label>_('search_to')"></label>&nbsp;&nbsp;&nbsp;&nbsp;</td>
<td height="20px">
<select id=":dayToName" name=":dayToName">
<option value="">--</option>
<option for="value in [str(v).zfill(2) for v in range(1, 32)]"
value=":value">:value</option>
</select> /
<select id=":monthToName" name=":monthToName">
<option value="">--</option>
<option for="value in [str(v).zfill(2) for v in range(1, 13)]"
value=":value">:value</option>
</select> /
<select id=":yearToName" name=":yearToName">
<option value="">--</option>
<option for="value in range(widget['startYear'],widget['endYear']+1)"
value=":value">:value</option>
</select>
<!-- The icon for displaying the calendar popup -->
<x if="widget['calendar']">
<input type="hidden" id=":toName" name=":toName"/>
<img id=":'%s_img' % toName" src=":%s/ui/calendar.gif' % appUrl"/>
<script type="text/javascript">:tool.getCalendarInit(toName, years)">
</script>
</x>
</td>
</tr>
</table>
</x>''')
# Required CSS and Javascript files for this type.
cssFiles = {'edit': ('jscalendar/calendar-blue.css',)}
jsFiles = {'edit': ('jscalendar/calendar.js',
'jscalendar/lang/calendar-en.js',
'jscalendar/calendar-setup.js')}
# Possible values for "format"
WITH_HOUR = 0
WITHOUT_HOUR = 1
dateParts = ('year', 'month', 'day')
hourParts = ('hour', 'minute')
def __init__(self, validator=None, multiplicity=(0,1), default=None,
format=WITH_HOUR, calendar=True,
startYear=time.localtime()[0]-10,
endYear=time.localtime()[0]+10, reverseYears=False,
show=True, page='main', group=None, layouts=None, move=0,
indexed=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, label=None,
sdefault=None, scolspan=1, swidth=None, sheight=None):
self.format = format
self.calendar = calendar
self.startYear = startYear
self.endYear = endYear
# If reverseYears is True, in the selection box, available years, from
# self.startYear to self.endYear will be listed in reverse order.
self.reverseYears = reverseYears
Field.__init__(self, validator, multiplicity, default, show, page,
group, layouts, move, indexed, searchable,
specificReadPermission, specificWritePermission, width,
height, None, colspan, master, masterValue, focus,
historized, True, mapping, label, sdefault, scolspan,
swidth, sheight)
def getCss(self, layoutType, res):
# CSS files are only required if the calendar must be shown.
if self.calendar: Field.getCss(self, layoutType, res)
def getJs(self, layoutType, res):
# Javascript files are only required if the calendar must be shown.
if self.calendar: Field.getJs(self, layoutType, res)
def getSelectableYears(self):
'''Gets the list of years one may select for this field.'''
res = range(self.startYear, self.endYear + 1)
if self.reverseYears: res.reverse()
return res
def validateValue(self, obj, value):
DateTime = obj.getProductConfig().DateTime
try:
value = DateTime(value)
except DateTime.DateError, ValueError:
return obj.translate('bad_date')
def getFormattedValue(self, obj, value, showChanges=False):
if self.isEmptyValue(value): return ''
tool = obj.getTool().appy()
# A problem may occur with some extreme year values. Replace the "year"
# part "by hand".
dateFormat = tool.dateFormat
if '%Y' in dateFormat:
dateFormat = dateFormat.replace('%Y', str(value.year()))
res = value.strftime(dateFormat)
if self.format == Date.WITH_HOUR:
res += ' %s' % value.strftime(tool.hourFormat)
return res
def getRequestValue(self, request, requestName=None):
name = requestName or self.name
# Manage the "date" part
value = ''
for part in self.dateParts:
valuePart = request.get('%s_%s' % (name, part), None)
if not valuePart: return None
value += valuePart + '/'
value = value[:-1]
# Manage the "hour" part
if self.format == self.WITH_HOUR:
value += ' '
for part in self.hourParts:
valuePart = request.get('%s_%s' % (name, part), None)
if not valuePart: return None
value += valuePart + ':'
value = value[:-1]
return value
def getStorableValue(self, value):
if not self.isEmptyValue(value):
import DateTime
return DateTime.DateTime(value)
def getIndexType(self): return 'DateIndex'
# ------------------------------------------------------------------------------

226
fields/file.py Normal file
View file

@ -0,0 +1,226 @@
# ------------------------------------------------------------------------------
# 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 time, os.path, mimetypes
from appy.fields import Field
from appy.px import Px
from appy.shared import utils as sutils
# ------------------------------------------------------------------------------
class File(Field):
pxView = pxCell = Px('''
<x var="info=contextObj.getFileInfo(value);
empty=not info['size'];
imgSrc='%s/download?name=%s' % (contextObj.absolute_url(), name)">
<x if="not empty and not widget['isImage']">
<a href=":imgSrc">:info['filename']"</a>&nbsp;&nbsp;-
<i class="discreet">'%sKb' % (info['size'] / 1024)"></i>
</x>
<x if="not empty and widget['isImage']"><img src=":imgSrc"/></x>
<x if="empty">-</x>
</x>''')
pxEdit = Px('''
<x var="info=contextObj.getFileInfo(value);
empty= not info['size'];
fName=q('%s_file' % name)">
<x if="not: empty">:widget['pxView']</x><br/>
<x if="not empty">
<!-- Keep the file unchanged. -->
<input type="radio" value="nochange"
checked=":(info['size'] != 0) and 'checked' or None"
name=":'%s_delete' % name" id=":'%s_nochange' % name"
onclick=":'document.getElementById(%s).disabled=true' % fName"/>
<label lfor=":'%s_nochange' % name">Keep the file unchanged</label><br/>
<!-- Delete the file. -->
<x if="not widget['required']">
<input type="radio" value="delete"
name=":'%s_delete' % name" id=":'%s_delete' % name"
onclick=":'document.getElementById(%s).disabled=true' % fName"/>
<label lfor=":'%s_delete' % name">Delete the file</label><br/>
</x>
<!-- Replace with a new file. -->
<input type="radio" value=""
checked=":(info['size'] == 0) and 'checked' or None"
name=":'%s_delete' % name" id=":'%s_upload' % name"
onclick=":'document.getElementById(%s).disabled=false' % fName"/>
<label lfor=":'%s_upload' % name">Replace it with a new file</label><br/>
</x>
<!-- The upload field. -->
<input type="file" name=":'%s_file' % name" id=":'%s_file' % name"
size=":widget['width']"/>
<script var="isDisabled=empty and 'false' or 'true'"
type="text/javascript">:document.getElementById(%s).disabled=%s'%\
(q(fName), q(isDisabled))">
</script>
</x>''')
pxSearch = ''
def __init__(self, validator=None, multiplicity=(0,1), default=None,
show=True, page='main', group=None, layouts=None, move=0,
indexed=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, label=None,
isImage=False, sdefault='', scolspan=1, swidth=None,
sheight=None):
self.isImage = isImage
Field.__init__(self, validator, multiplicity, default, show, page,
group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width,
height, None, colspan, master, masterValue, focus,
historized, True, mapping, label, sdefault, scolspan,
swidth, sheight)
@staticmethod
def getFileObject(filePath, fileName=None, zope=False):
'''Returns a File instance as can be stored in the database or
manipulated in code, filled with content from a file on disk,
located at p_filePath. If you want to give it a name that is more
sexy than the actual basename of filePath, specify it in
p_fileName.
If p_zope is True, it will be the raw Zope object = an instance of
OFS.Image.File. Else, it will be a FileWrapper instance from Appy.'''
f = file(filePath, 'rb')
if not fileName:
fileName = os.path.basename(filePath)
fileId = 'file.%f' % time.time()
import OFS.Image
res = OFS.Image.File(fileId, fileName, f)
res.filename = fileName
res.content_type = mimetypes.guess_type(fileName)[0]
f.close()
if not zope: res = sutils.FileWrapper(res)
return res
def getValue(self, obj):
value = Field.getValue(self, obj)
if value: value = sutils.FileWrapper(value)
return value
def getFormattedValue(self, obj, value, showChanges=False):
if not value: return value
return value._zopeFile
def getRequestValue(self, request, requestName=None):
name = requestName or self.name
return request.get('%s_file' % name)
def getDefaultLayouts(self): return {'view':'l-f','edit':'lrv-f'}
def isEmptyValue(self, value, obj=None):
'''Must p_value be considered as empty?'''
if not obj: return Field.isEmptyValue(self, value)
if value: return False
# If "nochange", the value must not be considered as empty
return obj.REQUEST.get('%s_delete' % self.name) != 'nochange'
imageExts = ('.jpg', '.jpeg', '.png', '.gif')
def validateValue(self, obj, value):
form = obj.REQUEST.form
action = '%s_delete' % self.name
if (not value or not value.filename) and form.has_key(action) and \
not form[action]:
# If this key is present but empty, it means that the user selected
# "replace the file with a new one". So in this case he must provide
# a new file to upload.
return obj.translate('file_required')
# Check that, if self.isImage, the uploaded file is really an image
if value and value.filename and self.isImage:
ext = os.path.splitext(value.filename)[1].lower()
if ext not in File.imageExts:
return obj.translate('image_required')
defaultMimeType = 'application/octet-stream'
def store(self, obj, value):
'''Stores the p_value that represents some file. p_value can be:
* an instance of Zope class ZPublisher.HTTPRequest.FileUpload. In
this case, it is file content coming from a HTTP POST;
* an instance of Zope class OFS.Image.File;
* an instance of appy.shared.utils.FileWrapper, which wraps an
instance of OFS.Image.File and adds useful methods for manipulating
it;
* a string. In this case, the string represents the path of a file
on disk;
* a 2-tuple (fileName, fileContent) where:
- fileName is the name of the file (ie "myFile.odt")
- fileContent is the binary or textual content of the file or an
open file handler.
* a 3-tuple (fileName, fileContent, mimeType) where
- fileName and fileContent have the same meaning than above;
- mimeType is the MIME type of the file.
'''
if value:
ZFileUpload = obj.o.getProductConfig().FileUpload
OFSImageFile = obj.o.getProductConfig().File
if isinstance(value, ZFileUpload):
# The file content comes from a HTTP POST.
# Retrieve the existing value, or create one if None
existingValue = getattr(obj.aq_base, self.name, None)
if not existingValue:
existingValue = OFSImageFile(self.name, '', '')
# Set mimetype
if value.headers.has_key('content-type'):
mimeType = value.headers['content-type']
else:
mimeType = File.defaultMimeType
existingValue.content_type = mimeType
# Set filename
fileName = value.filename
filename= fileName[max(fileName.rfind('/'),fileName.rfind('\\'),
fileName.rfind(':'))+1:]
existingValue.filename = fileName
# Set content
existingValue.manage_upload(value)
setattr(obj, self.name, existingValue)
elif isinstance(value, OFSImageFile):
setattr(obj, self.name, value)
elif isinstance(value, sutils.FileWrapper):
setattr(obj, self.name, value._zopeFile)
elif isinstance(value, basestring):
setattr(obj, self.name, File.getFileObject(value, zope=True))
elif type(value) in sutils.sequenceTypes:
# It should be a 2-tuple or 3-tuple
fileName = None
mimeType = None
if len(value) == 2:
fileName, fileContent = value
elif len(value) == 3:
fileName, fileContent, mimeType = value
else:
raise WRONG_FILE_TUPLE
if fileName:
fileId = 'file.%f' % time.time()
zopeFile = OFSImageFile(fileId, fileName, fileContent)
zopeFile.filename = fileName
if not mimeType:
mimeType = mimetypes.guess_type(fileName)[0]
zopeFile.content_type = mimeType
setattr(obj, self.name, zopeFile)
else:
# I store value "None", excepted if I find in the request the desire
# to keep the file unchanged.
action = None
rq = getattr(obj, 'REQUEST', None)
if rq: action = rq.get('%s_delete' % self.name, None)
if action == 'nochange': pass
else: setattr(obj, self.name, None)
# ------------------------------------------------------------------------------

105
fields/float.py Normal file
View file

@ -0,0 +1,105 @@
# ------------------------------------------------------------------------------
# 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/>.
# ------------------------------------------------------------------------------
from appy.fields import Field
from appy.px import Px
from appy.shared import utils as sutils
# ------------------------------------------------------------------------------
class Float(Field):
allowedDecimalSeps = (',', '.')
allowedThousandsSeps = (' ', '')
pxView = pxCell = Px('''
<x><x>:value</x>
<input type="hidden" if="masterCss" class=":masterCss" value=":value"
name=":name" id=":name"/>
</x>''')
pxEdit = Px('''
<input var="maxChars=widget['maxChars'] and widget['maxChars'] or ''"
id=":name" name=":name" size=":widget['width']" maxlength=":maxChars"
value=":inRequest and requestValue or value" type="text"/>''')
pxSearch = Px('''
<x var="maxChars=widget['maxChars'] and widget['maxChars'] or ''">
<label>:_(widget['labelId'])"></label><br/>&nbsp;&nbsp;
<!-- From -->
<x var="fromName='%s*float' % widgetName">
<label lfor=":fromName">:_('search_from')"></label>
<input type="text" name=":fromName" maxlength=":maxChars"
value=":widget['sdefault'][0]" size=":widget['swidth]"/>
</x>
<!-- To -->
<x var="toName='%s_to' % name">
<label lfor=":toName">:_('search_to')</label>
<input type="text" name=":toName" maxlength=":maxChars"
value=":widget['sdefault'][1]" size="widget['swidth']"/>
</x><br/>
</x>''')
def __init__(self, validator=None, multiplicity=(0,1), default=None,
show=True, page='main', group=None, layouts=None, move=0,
indexed=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=5, height=None,
maxChars=13, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, label=None,
sdefault=('',''), scolspan=1, swidth=None, sheight=None,
precision=None, sep=(',', '.'), tsep=' '):
# The precision is the number of decimal digits. This number is used
# for rendering the float, but the internal float representation is not
# rounded.
self.precision = precision
# The decimal separator can be a tuple if several are allowed, ie
# ('.', ',')
if type(sep) not in sutils.sequenceTypes:
self.sep = (sep,)
else:
self.sep = sep
# Check that the separator(s) are among allowed decimal separators
for sep in self.sep:
if sep not in Float.allowedDecimalSeps:
raise Exception('Char "%s" is not allowed as decimal ' \
'separator.' % sep)
self.tsep = tsep
Field.__init__(self, validator, multiplicity, default, show, page,
group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width,
height, maxChars, colspan, master, masterValue, focus,
historized, True, mapping, label, sdefault, scolspan,
swidth, sheight)
self.pythonType = float
def getFormattedValue(self, obj, value, showChanges=False):
return sutils.formatNumber(value, sep=self.sep[0],
precision=self.precision, tsep=self.tsep)
def validateValue(self, obj, value):
# Replace used separator with the Python separator '.'
for sep in self.sep: value = value.replace(sep, '.')
value = value.replace(self.tsep, '')
try:
value = self.pythonType(value)
except ValueError:
return obj.translate('bad_%s' % self.pythonType.__name__)
def getStorableValue(self, value):
if not self.isEmptyValue(value):
for sep in self.sep: value = value.replace(sep, '.')
value = value.replace(self.tsep, '')
return self.pythonType(value)
# ------------------------------------------------------------------------------

39
fields/info.py Normal file
View file

@ -0,0 +1,39 @@
# ------------------------------------------------------------------------------
# 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/>.
# ------------------------------------------------------------------------------
from appy.fields import Field
# ------------------------------------------------------------------------------
class Info(Field):
'''An info is a field whose purpose is to present information
(text, html...) to the user.'''
# An info only displays a label. So PX for showing content are empty.
pxView = pxEdit = pxCell = pxSearch = ''
def __init__(self, validator=None, multiplicity=(1,1), default=None,
show='view', page='main', group=None, layouts=None, move=0,
indexed=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, label=None):
Field.__init__(self, None, (0,1), default, show, page, group, layouts,
move, indexed, False, specificReadPermission,
specificWritePermission, width, height, None, colspan,
master, masterValue, focus, historized, False, mapping,
label, None, None, None, None)
self.validable = False
# ------------------------------------------------------------------------------

80
fields/integer.py Normal file
View file

@ -0,0 +1,80 @@
# ------------------------------------------------------------------------------
# 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/>.
# ------------------------------------------------------------------------------
from appy.fields import Field
from appy.px import Px
# ------------------------------------------------------------------------------
class Integer(Field):
pxView = pxCell = Px('''
<x><x>:value</x>
<input type="hidden" if="masterCss"
class=":masterCss" value=":value" name=":name" id=":name"/>
</x>''')
pxEdit = Px('''
<input var="maxChars=widget['maxChars'] and widget['maxChars'] or ''"
id=":name" name=":name" size=":widget['width']"
maxlength=":maxChars" value=":inRequest and requestValue or value"
type="text"/>''')
pxSearch = Px('''
<x var="maxChars= widget['maxChars'] and widget['maxChars'] or ''">
<label>:_(widget['labelId'])"></label><br/>&nbsp;&nbsp;
<!-- From -->
<x var="fromName='%s*int' % widgetName">
<label lfor=":fromName">:_('search_from')</label>
<input type="text" name=":fromName" maxlength=":maxChars"
value=":widget['sdefault'][0]" size=":widget['swidth']"/>
</x>
<!-- To -->
<x var="toName='%s_to' % name">
<label lfor=":toName">:_('search_to')"></label>
<input type="text" name=":toName" maxlength=":maxChars"
value=":widget['sdefault'][1]" size=":widget['swidth']"/>
</x><br/>
</x>''')
def __init__(self, validator=None, multiplicity=(0,1), default=None,
show=True, page='main', group=None, layouts=None, move=0,
indexed=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=5, height=None,
maxChars=13, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, label=None,
sdefault=('',''), scolspan=1, swidth=None, sheight=None):
Field.__init__(self, validator, multiplicity, default, show, page,
group, layouts, move, indexed, searchable,
specificReadPermission, specificWritePermission, width,
height, maxChars, colspan, master, masterValue, focus,
historized, True, mapping, label, sdefault, scolspan,
swidth, sheight)
self.pythonType = long
def validateValue(self, obj, value):
try:
value = self.pythonType(value)
except ValueError:
return obj.translate('bad_%s' % self.pythonType.__name__)
def getStorableValue(self, value):
if not self.isEmptyValue(value): return self.pythonType(value)
def getFormattedValue(self, obj, value, showChanges=False):
if self.isEmptyValue(value): return ''
return str(value)
# ------------------------------------------------------------------------------

166
fields/list.py Normal file
View file

@ -0,0 +1,166 @@
# ------------------------------------------------------------------------------
# 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/>.
# ------------------------------------------------------------------------------
from appy import Object
from appy.fields import Field
from appy.px import Px
from appy.gen.layout import Table
# ------------------------------------------------------------------------------
class List(Field):
'''A list.'''
# PX for rendering a single row.
pxRow = Px('''
<tr valign="top" style="(rowIndex==-1) and 'display: none' or ''">
<td align="center" for="fieldInfo in widget['fieldsd']">
<x var="widget=fieldInfo[1];
tagCss='noStyle';
widgetName='%s*%d' % (widget['name'], rowIndex)">
<x>:widget['pxView']</x>
</x>
</td>
<!-- Icon for removing the row -->
<td if="layoutType=='edit'" align=":dright">
<img style="cursor:pointer" src=":'%s/ui/delete.png' % appUrl"
title="Delete"
onclick=":'deleteRow(%s, this)' % q('list_%s' % name)"/>
</td>
</tr>''')
# PX for rendering the list (shared between pxView and pxEdit).
pxTable = Px('''
<table var="isEdit=layoutType == 'edit'" if="isEdit or value"
id=":'list_%s' % name" class="isEdit and 'grid' or 'list'">
<!-- Header -->
<tr valign="bottom">
<th for="fieldInfo in widget['fieldsd']">::_(fieldInfo[1]['labelId'])
</th>
<!-- Icon for adding a new row. -->
<th if="isEdit">
<img style="cursor:pointer" src=":'%s/ui/plus.png' % appUrl"
title=":_('add_ref')"
onclick=":'insertRow(%s)' % q('list_%s' % name)"/>
</th>
</tr>
<!-- Template row (edit only) -->
<x var="rowIndex=-1" if="isEdit">:widget['pxRow']</x>
<tr height="7px" if="isEdit"><td></td></tr>
<!-- Rows of data -->
<x var="rows =inRequest and requestValue or value" for="row in rows">
<x var="rowIndex=loop.row.nb">:widget['pxRow']</x>
</x>
</table>''')
pxView = pxCell = Px('''<x>:widget['pxTable']</x>''')
pxEdit = Px('''
<x>
<!-- This input makes Appy aware that this field is in the request -->
<input type="hidden" name=":name" value=""/>
<x>:widget['pxTable']</x>
</x>''')
pxSearch = ''
def __init__(self, fields, validator=None, multiplicity=(0,1), default=None,
show=True, page='main', group=None, layouts=None, move=0,
indexed=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, label=None,
subLayouts=Table('fv', width=None)):
Field.__init__(self, validator, multiplicity, default, show, page,
group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width,
height, None, colspan, master, masterValue, focus,
historized, True, mapping, label, None, None, None, None)
self.validable = True
# Tuples of (names, Field instances) determining the format of every
# element in the list.
self.fields = fields
self.fieldsd = [(n, f.__dict__) for (n,f) in self.fields]
# Force some layouting for sub-fields, if subLayouts are given. So the
# one who wants freedom on tuning layouts at the field level must
# specify subLayouts=None.
if subLayouts:
for name, field in self.fields:
field.layouts = field.formatLayouts(subLayouts)
def getField(self, name):
'''Gets the field definition whose name is p_name.'''
for n, field in self.fields:
if n == name: return field
def getRequestValue(self, request, requestName=None):
'''Concatenates the list from distinct form elements in the request.'''
name = requestName or self.name # A List may be into another List (?)
prefix = name + '*' + self.fields[0][0] + '*'
res = {}
for key in request.keys():
if not key.startswith(prefix): continue
# I have found a row. Gets its index
row = Object()
if '_' in key: key = key[:key.index('_')]
rowIndex = int(key.split('*')[-1])
if rowIndex == -1: continue # Ignore the template row.
for subName, subField in self.fields:
keyName = '%s*%s*%s' % (name, subName, rowIndex)
v = subField.getRequestValue(request, requestName=keyName)
setattr(row, subName, v)
res[rowIndex] = row
# Produce a sorted list.
keys = res.keys()
keys.sort()
res = [res[key] for key in keys]
# I store in the request this computed value. This way, when individual
# subFields will need to get their value, they will take it from here,
# instead of taking it from the specific request key. Indeed, specific
# request keys contain row indexes that may be wrong after row deletions
# by the user.
request.set(name, res)
return res
def getStorableValue(self, value):
'''Gets p_value in a form that can be stored in the database.'''
res = []
for v in value:
sv = Object()
for name, field in self.fields:
setattr(sv, name, field.getStorableValue(getattr(v, name)))
res.append(sv)
return res
def getInnerValue(self, outerValue, name, i):
'''Returns the value of inner field named p_name in row number p_i
within the whole list of values p_outerValue.'''
if i == -1: return ''
if not outerValue: return ''
if i >= len(outerValue): return ''
return getattr(outerValue[i], name, '')
def getCss(self, layoutType, res):
'''Gets the CSS required by sub-fields if any.'''
for name, field in self.fields:
field.getCss(layoutType, res)
def getJs(self, layoutType, res):
'''Gets the JS required by sub-fields if any.'''
for name, field in self.fields:
field.getJs(layoutType, res)
# ------------------------------------------------------------------------------

View file

@ -1,7 +1,8 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import sha import sha
from appy import Object from appy import Object
from appy.gen import Type from appy.gen import Field
from appy.px import Px
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class OgoneConfig: class OgoneConfig:
@ -25,20 +26,39 @@ class OgoneConfig:
def __repr__(self): return str(self.__dict__) def __repr__(self): return str(self.__dict__)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class Ogone(Type): class Ogone(Field):
'''This field allows to perform payments with the Ogone (r) system.''' '''This field allows to perform payments with the Ogone (r) system.'''
urlTypes = ('accept', 'decline', 'exception', 'cancel') urlTypes = ('accept', 'decline', 'exception', 'cancel')
pxView = pxCell = Px('''
<x>
<!-- var "value" is misused and contains the contact params for Ogone -->
<p>:value</p>
<!-- The form for sending the payment request to Ogone -->
<form method="post" id="form1" name="form1" var="env=value['env']"
action=":'https://secure.ogone.com/ncol/%s/orderstandard.asp'% env">
<x for="item in value.items()">
<input type="hidden" if="item[0] != 'env'" id=":item[0]"
name=":item[0]" value=":item[1]"/>
</x>
<!-- Submit image -->
<input type="image" id="submit2" name="submit2"
src=":'%s/ui/ogone.gif' % $appUrl" title=":_('custom_pay')"/>
</form>
</x>''')
pxEdit = pxSearch = ''
def __init__(self, orderMethod, responseMethod, show='view', page='main', def __init__(self, orderMethod, responseMethod, show='view', page='main',
group=None, layouts=None, move=0, specificReadPermission=False, group=None, layouts=None, move=0, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False, colspan=1, master=None, masterValue=None, focus=False,
mapping=None, label=None): mapping=None, label=None):
Type.__init__(self, None, (0,1), None, show, page, group, layouts, move, Field.__init__(self, None, (0,1), None, show, page, group, layouts,
False, False,specificReadPermission, move, False, False,specificReadPermission,
specificWritePermission, width, height, None, colspan, specificWritePermission, width, height, None, colspan,
master, masterValue, focus, False, True, mapping, label, master, masterValue, focus, False, True, mapping, label,
None, None, None, None) None, None, None, None)
# orderMethod must contain a method returning a dict containing info # orderMethod must contain a method returning a dict containing info
# about the order. Following keys are mandatory: # about the order. Following keys are mandatory:
# * orderID An identifier for the order. Don't use the object UID # * orderID An identifier for the order. Don't use the object UID

226
fields/pod.py Normal file
View file

@ -0,0 +1,226 @@
# ------------------------------------------------------------------------------
# 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 time, os, os.path, StringIO
from appy.fields import Field
from appy.px import Px
from file import File
from appy.gen.layout import Table
from appy.pod import PodError
from appy.pod.renderer import Renderer
from appy.shared import utils as sutils
# ------------------------------------------------------------------------------
class Pod(Field):
'''A pod is a field allowing to produce a (PDF, ODT, Word, RTF...) document
from data contained in Appy class and linked objects or anything you
want to put in it. It uses appy.pod.'''
# Layout for rendering a POD field for exporting query results.
rLayouts = {'view': Table('fl', width=None)}
POD_ERROR = 'An error occurred while generating the document. Please ' \
'contact the system administrator.'
DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.'
pxView = pxCell = Px('''
<x>
<!-- Ask action -->
<x if="widget['askAction']">
<x var="doLabel='%s_askaction' % widget['labelId'];
chekboxId='%s_%s_cb' % (contextObj.UID(), name)">
<input type="checkbox" name=":doLabel" id=":chekboxId"/>
<label lfor=":chekboxId" class="discreet">:_(doLabel)"></label>
</x>
</x>
<img for="podFormat in ztool.getPodInfo(contextObj, name)[1]"
src=":'%s/ui/%s.png' % (appUrl, podFormat)"
onclick=":'generatePodDocument(%s, %s, %s, %s)' % \
(q(contextObj.UID()), q(name), q(podFormat), \
q(ztool.getQueryInfo()))"
title=":podFormat.capitalize()" style="cursor:pointer"/>
</x>''')
pxEdit = pxSearch = ''
def __init__(self, validator=None, default=None, show=('view', 'result'),
page='main', group=None, layouts=None, move=0, indexed=False,
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, label=None,
template=None, context=None, action=None, askAction=False,
stylesMapping={}, freezeFormat='pdf'):
# The following param stores the path to a POD template
self.template = template
# The context is a dict containing a specific pod context, or a method
# that returns such a dict.
self.context = context
# Next one is a method that will be triggered after the document has
# been generated.
self.action = action
# If askAction is True, the action will be triggered only if the user
# checks a checkbox, which, by default, will be unchecked.
self.askAction = askAction
# A global styles mapping that would apply to the whole template
self.stylesMapping = stylesMapping
# Freeze format is by PDF by default
self.freezeFormat = freezeFormat
Field.__init__(self, None, (0,1), default, show, page, group, layouts,
move, indexed, searchable, specificReadPermission,
specificWritePermission, width, height, None, colspan,
master, masterValue, focus, historized, False, mapping,
label, None, None, None, None)
self.validable = False
def isFrozen(self, obj):
'''Is there a frozen document for p_self on p_obj?'''
value = getattr(obj.o.aq_base, self.name, None)
return isinstance(value, obj.o.getProductConfig().File)
def getToolInfo(self, obj):
'''Gets information related to this field (p_self) that is available in
the tool: the POD template and the available output formats. If this
field is frozen, available output formats are not available anymore:
only the format of the frozen doc is returned.'''
tool = obj.tool
appyClass = tool.o.getAppyClass(obj.o.meta_type)
# Get the output format(s)
if self.isFrozen(obj):
# The only available format is the one from the frozen document
fileName = getattr(obj.o.aq_base, self.name).filename
formats = (os.path.splitext(fileName)[1][1:],)
else:
# Available formats are those which are selected in the tool.
name = tool.getAttributeName('formats', appyClass, self.name)
formats = getattr(tool, name)
# Get the POD template
name = tool.getAttributeName('podTemplate', appyClass, self.name)
template = getattr(tool, name)
return (template, formats)
def getValue(self, obj):
'''Gets, on_obj, the value conforming to self's type definition. For a
Pod field, if a file is stored in the field, it means that the
field has been frozen. Else, it means that the value must be
retrieved by calling pod to compute the result.'''
rq = getattr(obj, 'REQUEST', None)
res = getattr(obj.aq_base, self.name, None)
if res and res.size:
# Return the frozen file.
return sutils.FileWrapper(res)
# If we are here, it means that we must call pod to compute the file.
# A Pod field differs from other field types because there can be
# several ways to produce the field value (ie: output file format can be
# odt, pdf,...; self.action can be executed or not...). We get those
# precisions about the way to produce the file from the request object
# and from the tool. If we don't find the request object (or if it does
# not exist, ie, when Zope runs in test mode), we use default values.
obj = obj.appy()
tool = obj.tool
# Get POD template and available formats from the tool.
template, availFormats = self.getToolInfo(obj)
# Get the output format
defaultFormat = 'pdf'
if defaultFormat not in availFormats: defaultFormat = availFormats[0]
outputFormat = getattr(rq, 'podFormat', defaultFormat)
# Get or compute the specific POD context
specificContext = None
if callable(self.context):
specificContext = self.callMethod(obj, self.context)
else:
specificContext = self.context
# Temporary file where to generate the result
tempFileName = '%s/%s_%f.%s' % (
sutils.getOsTempFolder(), obj.uid, time.time(), outputFormat)
# Define parameters to give to the appy.pod renderer
podContext = {'tool': tool, 'user': obj.user, 'self': obj, 'field':self,
'now': obj.o.getProductConfig().DateTime(),
'_': obj.translate, 'projectFolder': tool.getDiskFolder()}
# If the POD document is related to a query, get it from the request,
# execute it and put the result in the context.
isQueryRelated = rq.get('queryData', None)
if isQueryRelated:
# Retrieve query params from the request
cmd = ', '.join(tool.o.queryParamNames)
cmd += " = rq['queryData'].split(';')"
exec cmd
# (re-)execute the query, but without any limit on the number of
# results; return Appy objects.
objs = tool.o.executeQuery(obj.o.portal_type, searchName=search,
sortBy=sortKey, sortOrder=sortOrder, filterKey=filterKey,
filterValue=filterValue, maxResults='NO_LIMIT')
podContext['objects'] = [o.appy() for o in objs['objects']]
# Add the field-specific context if present.
if specificContext:
podContext.update(specificContext)
# If a custom param comes from the request, add it to the context. A
# custom param must have format "name:value". Custom params override any
# other value in the request, including values from the field-specific
# context.
customParams = rq.get('customParams', None)
if customParams:
paramsDict = eval(customParams)
podContext.update(paramsDict)
# Define a potential global styles mapping
if callable(self.stylesMapping):
stylesMapping = self.callMethod(obj, self.stylesMapping)
else:
stylesMapping = self.stylesMapping
rendererParams = {'template': StringIO.StringIO(template.content),
'context': podContext, 'result': tempFileName,
'stylesMapping': stylesMapping,
'imageResolver': tool.o.getApp()}
if tool.unoEnabledPython:
rendererParams['pythonWithUnoPath'] = tool.unoEnabledPython
if tool.openOfficePort:
rendererParams['ooPort'] = tool.openOfficePort
# Launch the renderer
try:
renderer = Renderer(**rendererParams)
renderer.run()
except PodError, pe:
if not os.path.exists(tempFileName):
# In some (most?) cases, when OO returns an error, the result is
# nevertheless generated.
obj.log(str(pe), type='error')
return Pod.POD_ERROR
# Give a friendly name for this file
fileName = obj.translate(self.labelId)
if not isQueryRelated:
# This is a POD for a single object: personalize the file name with
# the object title.
fileName = '%s-%s' % (obj.title, fileName)
fileName = tool.normalize(fileName) + '.' + outputFormat
# Get a FileWrapper instance from the temp file on the filesystem
res = File.getFileObject(tempFileName, fileName)
# Execute the related action if relevant
doAction = getattr(rq, 'askAction', False) in ('True', True)
if doAction and self.action: self.action(obj, podContext)
# Returns the doc and removes the temp file
try:
os.remove(tempFileName)
except OSError, oe:
obj.log(Pod.DELETE_TEMP_DOC_ERROR % str(oe), type='warning')
except IOError, ie:
obj.log(Pod.DELETE_TEMP_DOC_ERROR % str(ie), type='warning')
return res
def store(self, obj, value):
'''Stores (=freezes) a document (in p_value) in the field.'''
if isinstance(value, sutils.FileWrapper):
value = value._zopeFile
setattr(obj, self.name, value)
# ------------------------------------------------------------------------------

672
fields/ref.py Normal file
View file

@ -0,0 +1,672 @@
# ------------------------------------------------------------------------------
# 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 sys
from appy.fields import Field, No
from appy.px import Px
from appy.gen.layout import Table
from appy.gen import utils as gutils
from appy.shared import utils as sutils
# ------------------------------------------------------------------------------
class Ref(Field):
# Some default layouts. "w" stands for "wide": those layouts produce tables
# of Ref objects whose width is 100%.
wLayouts = Table('lrv-f', width='100%')
# "d" stands for "description": a description label is added.
wdLayouts = {'view': Table('l-d-f', width='100%')}
# This PX displays the title of a referenced object, with a link on it to
# reach the consult view for this object. If we are on a back reference, the
# link allows to reach the correct page where the forward reference is
# defined. If we are on a forward reference, the "nav" parameter is added to
# the URL for allowing to navigate from one object to the next/previous on
# ui/view.
pxObjectTitle = Px('''
<x var="navInfo='ref.%s.%s:%s.%d.%d' % (contextObj.UID(), fieldName, \
appyType['pageName'], loop.obj.nb + startNumber, totalNumber);
navInfo=not appyType['isBack'] and navInfo or '';
cssClass=obj.getCssFor('title')">
<x>::obj.getSupTitle(navInfo)</x>
<a var="pageName=appyType['isBack'] and appyType['backd']['pageName'] or \
'main';
fullUrl=obj.getUrl(page=pageName, nav=navInfo)"
href=":fullUrl" class=":cssClass">:(not includeShownInfo) and \
obj.Title() or contextObj.getReferenceLabel(fieldName, obj.appy())
</a><span name="subTitle" style=":showSubTitles and 'display:inline' or \
'display:none'">::obj.getSubTitle()"</span>
</x>''')
# This PX displays icons for triggering actions on a given referenced object
# (edit, delete, etc).
pxObjectActions = Px('''
<table class="noStyle" var="isBack=appyType['isBack']">
<tr>
<!-- Arrows for moving objects up or down -->
<td if=":not isBack and (len(objs)&gt;1) and changeOrder and canWrite">
<x var="objectIndex=contextObj.getAppyRefIndex(fieldName, obj);
ajaxBaseCall=navBaseCall.replace('**v**','%s,%s,{%s:%s,%s:%s}'%\
(q(startNumber), q('ChangeRefOrder'), q('refObjectUid'),
q(obj.UID()), q('move'), q('**v**')))">
<img if="objectIndex &gt; 0" style="cursor:pointer"
src=":'%s/ui/arrowUp.png' % $appUrl" title=":_('move_up')"
onclick=":ajaxBaseCall.replace('**v**', 'up')"/><img
style="cursor:pointer" if="objectIndex &lt; (totalNumber-1)"
src=":'%s/ui/arrowDown.png' % appUrl" title=":_('move_down')"
onclick=":ajaxBaseCall.replace('**v**', 'down')"/>
</x>
</td>
<!-- Workflow transitions -->
<td if="obj.showTransitions('result')">
<x var="targetObj=obj">:targetObj.appy().pxTransitions</x>
</td>
<!-- Edit -->
<td if="not appyType['noForm'] and obj.mayEdit() and appyType['delete']">
<a var="navInfo='ref.%s.%s:%s.%d.%d' % (contextObj.UID(), fieldName, \
appyType['pageName'], loop.obj.nb + startNumber, \
totalNumber)"
href=":obj.getUrl(mode='edit', page='main', nav=navInfo)">
<img src=":'%s/ui/edit.png' % appUrl" title=":_('object_edit')"/>
</a>
</td>
<!-- Delete -->
<td if="not isBack and appyType['delete'] and canWrite and \
obj.mayDelete()">
<img style="cursor:pointer" title=":_('object_delete')"
src=":'%s/ui/delete.png' % appUrl"
onclick=":'onDeleteObject(%s)' % q(obj.UID())"/>
</td>
<!-- Unlink -->
<td if="not isBack and appyType['unlink'] and canWrite">
<img style="cursor:pointer" title=":_('object_unlink')"
src=":'%s/ui/unlink.png' % appUrl"
onclick=":'onUnlinkObject(%s,%s,%s)' % (q(contextObj.UID()), \
q(appyType['name']), q(obj.UID()))"/>
</td>
</tr>
</table>''')
# Displays the button allowing to add a new object through a Ref field, if
# it has been declared as addable and if multiplicities allow it.
pxAdd = Px('''
<x if="showPlusIcon">
<input type="button" class="button"
var="navInfo='ref.%s.%s:%s.%d.%d' % (contextObj.UID(), \
fieldName, appyType['pageName'], 0, totalNumber);
formCall='window.location=%s' % \
q('%s/do?action=Create&amp;className=%s&amp;nav=%s' % \
(folder.absolute_url(), linkedPortalType, navInfo));
formCall=not appyType['addConfirm'] and formCall or \
'askConfirm(%s,%s,%s)' % (q('script'), q(formCall), \
q(addConfirmMsg));
noFormCall=navBaseCall.replace('**v**', \
'%d,%s' % (startNumber, q('CreateWithoutForm')));
noFormCall=not appyType['addConfirm'] and noFormCall or \
'askConfirm(%s, %s, %s)' % (q('script'), q(noFormCall), \
q(addConfirmMsg))"
style=":'background-image: url(%s/ui/buttonAdd.png)' % appUrl"
value=":_('add_ref')"
onclick=":appyType['noForm'] and noFormCall or formCall"/>
</x>''')
# This PX displays, in a cell header from a ref table, icons for sorting the
# ref field according to the field that corresponds to this column.
pxSortIcons = Px('''
<x var="ajaxBaseCall=navBaseCall.replace('**v**', '%s,%s,{%s:%s,%s:%s}' % \
(q(startNumber), q('SortReference'), q('sortKey'), \
q(widget['name']), q('reverse'), q('**v**')))"
if="changeOrder and canWrite and ztool.isSortable(widget['name'], \
objs[0].meta_type, 'ref')">
<img style="cursor:pointer" src=":'%s/ui/sortAsc.png' % appUrl"
onclick=":ajaxBaseCall.replace('**v**', 'False')"/>
<img style="cursor:pointer" src=":'%s/ui/sortDesc.png' % appUrl"
onclick=":ajaxBaseCall.replace('**v**', 'True')"/>
</x>''')
# This PX is called by a XmlHttpRequest (or directly by pxView) for
# displaying the referred objects of a reference field.
pxViewContent = Px('''
<div var="fieldName=req['fieldName'];
appyType=contextObj.getAppyType(fieldName, asDict=True);
innerRef=req.get('innerRef',False) == 'True';
ajaxHookId=contextObj.UID() + fieldName;
startNumber=int(req.get('%s_startNumber' % ajaxHookId, 0));
refObjects=contextObj.getAppyRefs(fieldName, startNumber);
objs=refObjects['objects'];
totalNumber=refObjects['totalNumber'];
batchSize=refObjects['batchSize'];
folder=contextObj.getCreateFolder();
linkedPortalType=ztool.getPortalType(appyType['klass']);
canWrite=not appyType['isBack'] and \
contextObj.allows(appyType['writePermission']);
showPlusIcon=contextObj.mayAddReference(fieldName);
atMostOneRef=(appyType['multiplicity'][1] == 1) and \
(len(objs)&lt;=1);
addConfirmMsg=appyType['addConfirm'] and \
_('%s_addConfirm' % appyType['labelId']) or '';
navBaseCall='askRefField(%s,%s,%s,%s,**v**)' % \
(q(ajaxHookId), q(contextObj.absolute_url()), \
q(fieldName), q(innerRef));
changeOrder=contextObj.callField(fieldName, \
'changeOrderEnabled', contextObj);
showSubTitles=req.get('showSubTitles', 'true') == 'true'"
id=":ajaxHookId">
<!-- The definition of "atMostOneRef" above may sound strange: we
shouldn't check the actual number of referenced objects. But for
back references people often forget to specify multiplicities. So
concretely, multiplicities (0,None) are coded as (0,1). -->
<x if="atMostOneRef">
<!-- Display a simplified widget if maximum number of referenced objects
is 1. -->
<table>
<tr valign="top">
<!-- If there is no object -->
<x if="not objs">
<td class="discreet">:_('no_ref')</td>
<td>:widget['pxAdd']</td>
</x>
<!-- If there is an object... -->
<x if="objs">
<x for="obj in objs">
<td var="includeShownInfo=True">:widget['pxObjectTitle']</td>
</x>
</x>
</tr>
</table>
</x>
<!-- Display a table in all other cases -->
<x if="not atMostOneRef">
<div if="not innerRef or showPlusIcon" style="margin-bottom: 4px">
(<x>:totalNumber</x>)
<x>:widget['pxAdd']</x>
<!-- The search button if field is queryable -->
<input if="objs and appyType['queryable']" type="button" class="button"
style=":'background-image: url(%s/ui/buttonSearch.png)' % appUrl"
value=":_('search_title')"
onclick=":'window.location=%s' % \
q('%s/ui/search?className=%s&amp;ref=%s:%s' % \
(ztool.absolute_url(), linkedPortalType, contextObj.UID(), \
appyType['name']))"/>
</div>
<!-- Appy (top) navigation -->
<x>:contextObj.appy().pxAppyNavigate</x>
<!-- No object is present -->
<p class="discreet" if="not objs">:_('no_ref')</p>
<table if="objs" class=":innerRef and 'innerAppyTable' or ''"
width="100%">
<tr valign="bottom">
<td>
<!-- Show forward or backward reference(s) -->
<table class="not innerRef and 'list' or '';
width=innerRef and '100%' or \
appyType['layouts']['view']['width']"
var="columns=objs[0].getColumnsSpecifiers(\
appyType['shownInfo'], dir)">
<tr if="appyType['showHeaders']">
<th for="column in columns" width=":column['width']"
align="column['align']">
<x var="widget=column['field']">
<span>_(widget['labelId'])</span>
<x>:widget['pxSortIcons']</x>
<x var="className=linkedPortalType">:contextObj.appy(\
).pxShowDetails</x>
</x>
</th>
</tr>
<x for="obj in objs">
<tr valign="top" var="odd=loop.obj.odd"
class=":odd and 'even' or 'odd'">
<td for="column in columns"
width=":column['width']" align=":column['align']">
<x var="widget=column['field']">
<!-- The "title" field -->
<x if="python: widget['name'] == 'title'">
<x>:widget['pxObjectTitle']</x>
<div if="obj.mayAct()">:widget['pxObjectActions']</div>
</x>
<!-- Any other field -->
<x if="widget['name'] != 'title'">
<x var="contextObj=obj;
layoutType='cell';
innerRef=True"
if="obj.showField(widget['name'], layoutType='result')">
<!-- use-macro="app/ui/widgets/show/macros/field"/-->
</x>
</x>
</x>
</td>
</tr>
</x>
</table>
</td>
</tr>
</table>
<!-- Appy (bottom) navigation -->
<x>:contextObj.appy().pxAppyNavigate</x>
</x>
</div>''')
pxView = pxCell = Px('''
<x var="x=req.set('fieldName', widget['name'])">:widget['pxViewContent']
</x>''')
pxEdit = Px('''
<x if="widget['link']"
var="requestValue=req.get(name, []);
inRequest=req.has_key(name);
allObjects=contextObj.getSelectableAppyRefs(name);
refUids=[o.UID() for o in contextObj.getAppyRefs(name)['objects']];
isBeingCreated=contextObj.isTemporary()">
<select name=":name" size="isMultiple and widget['height'] or ''"
multiple="isMultiple and 'multiple' or ''">
<option value="" if="not isMultiple">:_('choose_a_value')"></option>
<x for="refObj in allObjects">
<option var="uid=contextObj.getReferenceUid(refObj)"
selected=":inRequest and (uid in requestValue) or \
(uid in refUids)"
value=":uid">:contextObj.getReferenceLabel(name, refObj)
</option>
</x>
</select>
</x>''')
pxSearch = Px('''
<x>
<label lfor=":widgetName">:_(widget['labelId'])"></label><br/>&nbsp;&nbsp;
<!-- The "and" / "or" radio buttons -->
<x var="operName='o_%s' % name;
orName='%s_or' % operName;
andName='%s_and' % operName"
if="widget['multiplicity'][1] != 1">
<input type="radio" name=":operName" id=":orName"
checked="checked" value="or"/>
<label lfor=":orName">:_('search_or')"></label>
<input type="radio" name=":operName" id=":andName" value="and"/>
<label lfor=":andName">:_('search_and')"></label><br/>
</x>
<!-- The list of values -->
<select name=":widgetName" size="widget['sheight']" multiple="multiple">
<x for="v in ztool.getSearchValues(name, className)">
<option var="uid=v[0];
title=ztool.getReferenceLabel(name, v[1], className)"
value=":uid"
title=":title">:ztool.truncateValue(title, widget['swidth'])">
</option>
</x>
</select>
</x>''')
def __init__(self, klass=None, attribute=None, validator=None,
multiplicity=(0,1), default=None, add=False, addConfirm=False,
delete=None, noForm=False, link=True, unlink=None, back=None,
show=True, page='main', group=None, layouts=None,
showHeaders=False, shownInfo=(), select=None, maxPerPage=30,
move=0, indexed=False, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=5, maxChars=None, colspan=1, master=None,
masterValue=None, focus=False, historized=False, mapping=None,
label=None, queryable=False, queryFields=None, queryNbCols=1,
navigable=False, searchSelect=None, changeOrder=True,
sdefault='', scolspan=1, swidth=None, sheight=None):
self.klass = klass
self.attribute = attribute
# May the user add new objects through this ref ?
self.add = add
# When the user adds a new object, must a confirmation popup be shown?
self.addConfirm = addConfirm
# May the user delete objects via this Ref?
self.delete = delete
if delete == None:
# By default, one may delete objects via a Ref for which one can
# add objects.
self.delete = bool(self.add)
# If noForm is True, when clicking to create an object through this ref,
# the object will be created automatically, and no creation form will
# be presented to the user.
self.noForm = noForm
# May the user link existing objects through this ref?
self.link = link
# May the user unlink existing objects?
self.unlink = unlink
if unlink == None:
# By default, one may unlink objects via a Ref for which one can
# link objects.
self.unlink = bool(self.link)
self.back = None
if back:
# It is a forward reference
self.isBack = False
# Initialise the backward reference
self.back = back
self.backd = back.__dict__
back.isBack = True
back.back = self
back.backd = self.__dict__
# klass may be None in the case we are defining an auto-Ref to the
# same class as the class where this field is defined. In this case,
# when defining the field within the class, write
# myField = Ref(None, ...)
# and, at the end of the class definition (name it K), write:
# K.myField.klass = K
# setattr(K, K.myField.back.attribute, K.myField.back)
if klass: setattr(klass, back.attribute, back)
# When displaying a tabular list of referenced objects, must we show
# the table headers?
self.showHeaders = showHeaders
# When displaying referenced object(s), we will display its title + all
# other fields whose names are listed in the following attribute.
self.shownInfo = list(shownInfo)
if not self.shownInfo: self.shownInfo.append('title')
# If a method is defined in this field "select", it will be used to
# filter the list of available tied objects.
self.select = select
# Maximum number of referenced objects shown at once.
self.maxPerPage = maxPerPage
# Specifies sync
sync = {'view': False, 'edit':True}
# If param p_queryable is True, the user will be able to perform queries
# from the UI within referenced objects.
self.queryable = queryable
# Here is the list of fields that will appear on the search screen.
# If None is specified, by default we take every indexed field
# defined on referenced objects' class.
self.queryFields = queryFields
# The search screen will have this number of columns
self.queryNbCols = queryNbCols
# Within the portlet, will referred elements appear ?
self.navigable = navigable
# The search select method is used if self.indexed is True. In this
# case, we need to know among which values we can search on this field,
# in the search screen. Those values are returned by self.searchSelect,
# which must be a static method accepting the tool as single arg.
self.searchSelect = searchSelect
# If changeOrder is False, it even if the user has the right to modify
# the field, it will not be possible to move objects or sort them.
self.changeOrder = changeOrder
Field.__init__(self, validator, multiplicity, default, show, page,
group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width,
height, None, colspan, master, masterValue, focus,
historized, sync, mapping, label, sdefault, scolspan,
swidth, sheight)
self.validable = self.link
def getDefaultLayouts(self):
return {'view': Table('l-f', width='100%'), 'edit': 'lrv-f'}
def isShowable(self, obj, layoutType):
res = Field.isShowable(self, obj, layoutType)
if not res: return res
# We add here specific Ref rules for preventing to show the field under
# some inappropriate circumstances.
if (layoutType == 'edit') and \
(self.mayAdd(obj) or not self.link): return False
if self.isBack:
if layoutType == 'edit': return False
else: return getattr(obj.aq_base, self.name, None)
return res
def getValue(self, obj, type='objects', noListIfSingleObj=False,
startNumber=None, someObjects=False):
'''Returns the objects linked to p_obj through this Ref field.
- If p_type is "objects", it returns the Appy wrappers;
- If p_type is "zobjects", it returns the Zope objects;
- If p_type is "uids", it returns UIDs of objects (= strings).
* If p_startNumber is None, it returns all referred objects.
* If p_startNumber is a number, it returns self.maxPerPage objects,
starting at p_startNumber.
If p_noListIfSingleObj is True, it returns the single reference as
an object and not as a list.
If p_someObjects is True, it returns an instance of SomeObjects
instead of returning a list of references.'''
uids = getattr(obj.aq_base, self.name, [])
if not uids:
# Maybe is there a default value?
defValue = Field.getValue(self, obj)
if defValue:
# I must prefix call to function "type" with "__builtins__"
# because this name was overridden by a method parameter.
if __builtins__['type'](defValue) in sutils.sequenceTypes:
uids = [o.o.UID() for o in defValue]
else:
uids = [defValue.o.UID()]
# Prepare the result: an instance of SomeObjects, that will be unwrapped
# if not required.
res = gutils.SomeObjects()
res.totalNumber = res.batchSize = len(uids)
batchNeeded = startNumber != None
if batchNeeded:
res.batchSize = self.maxPerPage
if startNumber != None:
res.startNumber = startNumber
# Get the objects given their uids
i = res.startNumber
while i < (res.startNumber + res.batchSize):
if i >= res.totalNumber: break
# Retrieve every reference in the correct format according to p_type
if type == 'uids':
ref = uids[i]
else:
ref = obj.getTool().getObject(uids[i])
if type == 'objects':
ref = ref.appy()
res.objects.append(ref)
i += 1
# Manage parameter p_noListIfSingleObj
if res.objects and noListIfSingleObj:
if self.multiplicity[1] == 1:
res.objects = res.objects[0]
if someObjects: return res
return res.objects
def getFormattedValue(self, obj, value, showChanges=False):
return value
def getIndexType(self): return 'ListIndex'
def getIndexValue(self, obj, forSearch=False):
'''Value for indexing is the list of UIDs of linked objects. If
p_forSearch is True, it will return a list of the linked objects'
titles instead.'''
if not forSearch:
res = getattr(obj.aq_base, self.name, [])
if res:
# The index does not like persistent lists.
res = list(res)
else:
# Ugly catalog: if I return an empty list, the previous value
# is kept.
res.append('')
return res
else:
# For the global search: return linked objects' titles.
res = [o.title for o in self.getValue(type='objects')]
if not res: res.append('')
return res
def validateValue(self, obj, value):
if not self.link: return None
# We only check "link" Refs because in edit views, "add" Refs are
# not visible. So if we check "add" Refs, on an "edit" view we will
# believe that that there is no referred object even if there is.
# If the field is a reference, we must ensure itself that multiplicities
# are enforced.
if not value:
nbOfRefs = 0
elif isinstance(value, basestring):
nbOfRefs = 1
else:
nbOfRefs = len(value)
minRef = self.multiplicity[0]
maxRef = self.multiplicity[1]
if maxRef == None:
maxRef = sys.maxint
if nbOfRefs < minRef:
return obj.translate('min_ref_violated')
elif nbOfRefs > maxRef:
return obj.translate('max_ref_violated')
def linkObject(self, obj, value, back=False):
'''This method links p_value (which can be a list of objects) to p_obj
through this Ref field.'''
# p_value can be a list of objects
if type(value) in sutils.sequenceTypes:
for v in value: self.linkObject(obj, v, back=back)
return
# Gets the list of referred objects (=list of uids), or create it.
obj = obj.o
refs = getattr(obj.aq_base, self.name, None)
if refs == None:
refs = obj.getProductConfig().PersistentList()
setattr(obj, self.name, refs)
# Insert p_value into it.
uid = value.o.UID()
if uid not in refs:
# Where must we insert the object? At the start? At the end?
if callable(self.add):
add = self.callMethod(obj, self.add)
else:
add = self.add
if add == 'start':
refs.insert(0, uid)
else:
refs.append(uid)
# Update the back reference
if not back: self.back.linkObject(value, obj, back=True)
def unlinkObject(self, obj, value, back=False):
'''This method unlinks p_value (which can be a list of objects) from
p_obj through this Ref field.'''
# p_value can be a list of objects
if type(value) in sutils.sequenceTypes:
for v in value: self.unlinkObject(obj, v, back=back)
return
obj = obj.o
refs = getattr(obj.aq_base, self.name, None)
if not refs: return
# Unlink p_value
uid = value.o.UID()
if uid in refs:
refs.remove(uid)
# Update the back reference
if not back: self.back.unlinkObject(value, obj, back=True)
def store(self, obj, value):
'''Stores on p_obj, the p_value, which can be:
* None;
* an object UID (=string);
* a list of object UIDs (=list of strings). Generally, UIDs or lists
of UIDs come from Ref fields with link:True edited through the web;
* a Zope object;
* a Appy object;
* a list of Appy or Zope objects.'''
# Standardize p_value into a list of Zope objects
objects = value
if not objects: objects = []
if type(objects) not in sutils.sequenceTypes: objects = [objects]
tool = obj.getTool()
for i in range(len(objects)):
if isinstance(objects[i], basestring):
# We have a UID here
objects[i] = tool.getObject(objects[i])
else:
# Be sure to have a Zope object
objects[i] = objects[i].o
uids = [o.UID() for o in objects]
# Unlink objects that are not referred anymore
refs = getattr(obj.aq_base, self.name, None)
if refs:
i = len(refs)-1
while i >= 0:
if refs[i] not in uids:
# Object having this UID must unlink p_obj
self.back.unlinkObject(tool.getObject(refs[i]), obj)
i -= 1
# Link new objects
if objects:
self.linkObject(obj, objects)
def mayAdd(self, obj):
'''May the user create a new referred object from p_obj via this Ref?'''
# We can't (yet) do that on back references.
if self.isBack: return No('is_back')
# Check if this Ref is addable
if callable(self.add):
add = self.callMethod(obj, self.add)
else:
add = self.add
if not add: return No('no_add')
# Have we reached the maximum number of referred elements?
if self.multiplicity[1] != None:
refCount = len(getattr(obj, self.name, ()))
if refCount >= self.multiplicity[1]: return No('max_reached')
# May the user edit this Ref field?
if not obj.allows(self.writePermission): return No('no_write_perm')
# Have the user the correct add permission?
tool = obj.getTool()
addPermission = '%s: Add %s' % (tool.getAppName(),
tool.getPortalType(self.klass))
folder = obj.getCreateFolder()
if not obj.getUser().has_permission(addPermission, folder):
return No('no_add_perm')
return True
def checkAdd(self, obj):
'''Compute m_mayAdd above, and raise an Unauthorized exception if
m_mayAdd returns False.'''
may = self.mayAdd(obj)
if not may:
from AccessControl import Unauthorized
raise Unauthorized("User can't write Ref field '%s' (%s)." % \
(self.name, may.msg))
def changeOrderEnabled(self, obj):
'''Is changeOrder enabled?'''
if isinstance(self.changeOrder, bool):
return self.changeOrder
else:
return self.callMethod(obj, self.changeOrder)
def autoref(klass, field):
'''klass.field is a Ref to p_klass. This kind of auto-reference can't be
declared in the "normal" way, like this:
class A:
attr1 = Ref(A)
because at the time Python encounters the static declaration
"attr1 = Ref(A)", class A is not completely defined yet.
This method allows to overcome this problem. You can write such
auto-reference like this:
class A:
attr1 = Ref(None)
autoref(A, A.attr1)
'''
field.klass = klass
setattr(klass, field.back.attribute, field.back)
# ------------------------------------------------------------------------------

506
fields/string.py Normal file
View file

@ -0,0 +1,506 @@
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
# 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 re, random
from appy.gen.layout import Table
from appy.gen.indexer import XhtmlTextExtractor
from appy.fields import Field
from appy.shared.data import countries
from appy.shared.xml_parser import XhtmlCleaner
from appy.shared.diff import HtmlDiff
from appy.shared import utils as sutils
# ------------------------------------------------------------------------------
digit = re.compile('[0-9]')
alpha = re.compile('[a-zA-Z0-9]')
letter = re.compile('[a-zA-Z]')
digits = '0123456789'
letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
emptyTuple = ()
# ------------------------------------------------------------------------------
class Selection:
'''Instances of this class may be given as validator of a String, in order
to tell Appy that the validator is a selection that will be computed
dynamically.'''
def __init__(self, methodName):
# The p_methodName parameter must be the name of a method that will be
# called every time Appy will need to get the list of possible values
# for the related field. It must correspond to an instance method of
# the class defining the related field. This method accepts no argument
# and must return a list (or tuple) of pairs (lists or tuples):
# (id, text), where "id" is one of the possible values for the
# field, and "text" is the value as will be shown on the screen.
# You can use self.translate within this method to produce an
# internationalized version of "text" if needed.
self.methodName = methodName
def getText(self, obj, value, appyType):
'''Gets the text that corresponds to p_value.'''
for v, text in appyType.getPossibleValues(obj, withTranslations=True):
if v == value:
return text
return value
# ------------------------------------------------------------------------------
class String(Field):
# Javascript files sometimes required by this type
jsFiles = {'edit': ('ckeditor/ckeditor.js',),
'view': ('ckeditor/ckeditor.js',)}
# Some predefined regular expressions that may be used as validators
c = re.compile
EMAIL = c('[a-zA-Z][\w\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.' \
'[a-zA-Z][a-zA-Z\.]*[a-zA-Z]')
ALPHANUMERIC = c('[\w-]+')
URL = c('(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*(\.[a-z]{2,5})?' \
'(([0-9]{1,5})?\/.*)?')
# Some predefined functions that may also be used as validators
@staticmethod
def _MODULO_97(obj, value, complement=False):
'''p_value must be a string representing a number, like a bank account.
this function checks that the 2 last digits are the result of
computing the modulo 97 of the previous digits. Any non-digit
character is ignored. If p_complement is True, it does compute the
complement of modulo 97 instead of modulo 97. p_obj is not used;
it will be given by the Appy validation machinery, so it must be
specified as parameter. The function returns True if the check is
successful.'''
if not value: return True
# First, remove any non-digit char
v = ''
for c in value:
if digit.match(c): v += c
# There must be at least 3 digits for performing the check
if len(v) < 3: return False
# Separate the real number from the check digits
number = int(v[:-2])
checkNumber = int(v[-2:])
# Perform the check
if complement:
return (97 - (number % 97)) == checkNumber
else:
# The check number can't be 0. In this case, we force it to be 97.
# This is the way Belgian bank account numbers work. I hope this
# behaviour is general enough to be implemented here.
mod97 = (number % 97)
if mod97 == 0: return checkNumber == 97
else: return checkNumber == mod97
@staticmethod
def MODULO_97(obj, value): return String._MODULO_97(obj, value)
@staticmethod
def MODULO_97_COMPLEMENT(obj, value):
return String._MODULO_97(obj, value, True)
BELGIAN_ENTERPRISE_NUMBER = MODULO_97_COMPLEMENT
@staticmethod
def IBAN(obj, value):
'''Checks that p_value corresponds to a valid IBAN number. IBAN stands
for International Bank Account Number (ISO 13616). If the number is
valid, the method returns True.'''
if not value: return True
# First, remove any non-digit or non-letter char
v = ''
for c in value:
if alpha.match(c): v += c
# Maximum size is 34 chars
if (len(v) < 8) or (len(v) > 34): return False
# 2 first chars must be a valid country code
if not countries.exists(v[:2].upper()): return False
# 2 next chars are a control code whose value must be between 0 and 96.
try:
code = int(v[2:4])
if (code < 0) or (code > 96): return False
except ValueError:
return False
# Perform the checksum
vv = v[4:] + v[:4] # Put the 4 first chars at the end.
nv = ''
for c in vv:
# Convert each letter into a number (A=10, B=11, etc)
# Ascii code for a is 65, so A=10 if we perform "minus 55"
if letter.match(c): nv += str(ord(c.upper()) - 55)
else: nv += c
return int(nv) % 97 == 1
@staticmethod
def BIC(obj, value):
'''Checks that p_value corresponds to a valid BIC number. BIC stands
for Bank Identifier Code (ISO 9362). If the number is valid, the
method returns True.'''
if not value: return True
# BIC number must be 8 or 11 chars
if len(value) not in (8, 11): return False
# 4 first chars, representing bank name, must be letters
for c in value[:4]:
if not letter.match(c): return False
# 2 next chars must be a valid country code
if not countries.exists(value[4:6].upper()): return False
# Last chars represent some location within a country (a city, a
# province...). They can only be letters or figures.
for c in value[6:]:
if not alpha.match(c): return False
return True
# Possible values for "format"
LINE = 0
TEXT = 1
XHTML = 2
PASSWORD = 3
CAPTCHA = 4
def __init__(self, validator=None, multiplicity=(0,1), default=None,
format=LINE, show=True, page='main', group=None, layouts=None,
move=0, indexed=False, searchable=False,
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, maxChars=None, colspan=1, master=None,
masterValue=None, focus=False, historized=False, mapping=None,
label=None, sdefault='', scolspan=1, swidth=None, sheight=None,
transform='none', styles=('p','h1','h2','h3','h4'),
allowImageUpload=True, inlineEdit=False):
# According to format, the widget will be different: input field,
# textarea, inline editor... Note that there can be only one String
# field of format CAPTCHA by page, because the captcha challenge is
# stored in the session at some global key.
self.format = format
self.isUrl = validator == String.URL
# When format is XHTML, the list of styles that the user will be able to
# select in the styles dropdown is defined hereafter.
self.styles = styles
# When format is XHTML, do we allow the user to upload images in it ?
self.allowImageUpload = allowImageUpload
# When format in XHTML, can the field be inline-edited (ckeditor)?
self.inlineEdit = inlineEdit
# The following field has a direct impact on the text entered by the
# user. It applies a transformation on it, exactly as does the CSS
# "text-transform" property. Allowed values are those allowed for the
# CSS property: "none" (default), "uppercase", "capitalize" or
# "lowercase".
self.transform = transform
Field.__init__(self, validator, multiplicity, default, show, page,
group, layouts, move, indexed, searchable,
specificReadPermission, specificWritePermission, width,
height, maxChars, colspan, master, masterValue, focus,
historized, True, mapping, label, sdefault, scolspan,
swidth, sheight)
self.isSelect = self.isSelection()
# If self.isSelect, self.sdefault must be a list of value(s).
if self.isSelect and not sdefault:
self.sdefault = []
# Default width, height and maxChars vary according to String format
if width == None:
if format == String.TEXT: self.width = 60
# This width corresponds to the standard width of an Appy page.
if format == String.XHTML: self.width = None
else: self.width = 30
if height == None:
if format == String.TEXT: self.height = 5
elif self.isSelect: self.height = 4
else: self.height = 1
if maxChars == None:
if self.isSelect: pass
elif format == String.LINE: self.maxChars = 256
elif format == String.TEXT: self.maxChars = 9999
elif format == String.XHTML: self.maxChars = 99999
elif format == String.PASSWORD: self.maxChars = 20
self.filterable = self.indexed and (self.format == String.LINE) and \
not self.isSelect
self.swidth = self.swidth or self.width
self.sheight = self.sheight or self.height
def isSelection(self):
'''Does the validator of this type definition define a list of values
into which the user must select one or more values?'''
res = True
if type(self.validator) in (list, tuple):
for elem in self.validator:
if not isinstance(elem, basestring):
res = False
break
else:
if not isinstance(self.validator, Selection):
res = False
return res
def getDefaultLayouts(self):
'''Returns the default layouts for this type. Default layouts can vary
acccording to format, multiplicity or history.'''
if self.format == String.TEXT:
return {'view': 'l-f', 'edit': 'lrv-d-f'}
elif self.format == String.XHTML:
if self.historized:
# self.historized can be a method or a boolean. If it is a
# method, it means that under some condition, historization will
# be enabled. So we come here also in this case.
view = 'lc-f'
else:
view = 'l-f'
return {'view': Table(view, width='100%'), 'edit': 'lrv-d-f'}
elif self.isMultiValued():
return {'view': 'l-f', 'edit': 'lrv-f'}
def getValue(self, obj):
# Cheat if this field represents p_obj's state
if self.name == 'state': return obj.State()
value = Field.getValue(self, obj)
if not value:
if self.isMultiValued(): return emptyTuple
else: return value
if isinstance(value, basestring) and self.isMultiValued():
value = [value]
elif isinstance(value, tuple):
value = list(value)
return value
def store(self, obj, value):
'''When the value is XHTML, we perform some cleanup.'''
if (self.format == String.XHTML) and value:
# When image upload is allowed, ckeditor inserts some "style" attrs
# (ie for image size when images are resized). So in this case we
# can't remove style-related information.
try:
value = XhtmlCleaner(keepStyles=False).clean(value)
except XhtmlCleaner.Error, e:
# Errors while parsing p_value can't prevent the user from
# storing it.
obj.log('Unparsable XHTML content in field "%s".' % self.name,
type='warning')
Field.store(self, obj, value)
def getDiffValue(self, obj, value):
'''Returns a version of p_value that includes the cumulative diffs
between successive versions.'''
res = None
lastEvent = None
for event in obj.workflow_history.values()[0]:
if event['action'] != '_datachange_': continue
if self.name not in event['changes']: continue
if res == None:
# We have found the first version of the field
res = event['changes'][self.name][0] or ''
else:
# We need to produce the difference between current result and
# this version.
iMsg, dMsg = obj.getHistoryTexts(lastEvent)
thisVersion = event['changes'][self.name][0] or ''
comparator = HtmlDiff(res, thisVersion, iMsg, dMsg)
res = comparator.get()
lastEvent = event
# Now we need to compare the result with the current version.
iMsg, dMsg = obj.getHistoryTexts(lastEvent)
comparator = HtmlDiff(res, value or '', iMsg, dMsg)
return comparator.get()
def getFormattedValue(self, obj, value, showChanges=False):
if self.isEmptyValue(value): return ''
res = value
if self.isSelect:
if isinstance(self.validator, Selection):
# Value(s) come from a dynamic vocabulary
val = self.validator
if self.isMultiValued():
return [val.getText(obj, v, self) for v in value]
else:
return val.getText(obj, value, self)
else:
# Value(s) come from a fixed vocabulary whose texts are in
# i18n files.
t = obj.translate
if self.isMultiValued():
res = [t('%s_list_%s' % (self.labelId, v)) for v in value]
else:
res = t('%s_list_%s' % (self.labelId, value))
elif (self.format == String.XHTML) and showChanges:
# Compute the successive changes that occurred on p_value.
res = self.getDiffValue(obj, res)
# If value starts with a carriage return, add a space; else, it will
# be ignored.
if isinstance(res, basestring) and \
(res.startswith('\n') or res.startswith('\r\n')): res = ' ' + res
return res
emptyStringTuple = ('',)
emptyValuesCatalogIgnored = (None, '')
def getIndexValue(self, obj, forSearch=False):
'''For indexing purposes, we return only strings, not unicodes.'''
res = Field.getIndexValue(self, obj, forSearch)
if isinstance(res, unicode):
res = res.encode('utf-8')
if res and forSearch and (self.format == String.XHTML):
# Convert the value to simple text.
extractor = XhtmlTextExtractor(raiseOnError=False)
res = extractor.parse('<p>%s</p>' % res)
# Ugly catalog: if I give an empty tuple as index value, it keeps the
# previous value. If I give him a tuple containing an empty string, it
# is ok.
if isinstance(res, tuple) and not res: res = self.emptyStringTuple
# Ugly catalog: if value is an empty string or None, it keeps the
# previous index value.
if res in self.emptyValuesCatalogIgnored: res = ' '
return res
def getPossibleValues(self,obj,withTranslations=False,withBlankValue=False):
'''Returns the list of possible values for this field if it is a
selection field. If p_withTranslations is True,
instead of returning a list of string values, the result is a list
of tuples (s_value, s_translation). If p_withBlankValue is True, a
blank value is prepended to the list, excepted if the type is
multivalued.'''
if not self.isSelect: raise 'This field is not a selection.'
if isinstance(self.validator, Selection):
# We need to call self.methodName for getting the (dynamic) values.
# If methodName begins with _appy_, it is a special Appy method:
# we will call it on the Mixin (=p_obj) directly. Else, it is a
# user method: we will call it on the wrapper (p_obj.appy()). Some
# args can be hidden into p_methodName, separated with stars,
# like in this example: method1*arg1*arg2. Only string params are
# supported.
methodName = self.validator.methodName
# Unwrap parameters if any.
if methodName.find('*') != -1:
elems = methodName.split('*')
methodName = elems[0]
args = elems[1:]
else:
args = ()
# On what object must we call the method that will produce the
# values?
if methodName.startswith('tool:'):
obj = obj.getTool()
methodName = methodName[5:]
# Do we need to call the method on the object or on the wrapper?
if methodName.startswith('_appy_'):
exec 'res = obj.%s(*args)' % methodName
else:
exec 'res = obj.appy().%s(*args)' % methodName
if not withTranslations: res = [v[0] for v in res]
elif isinstance(res, list): res = res[:]
else:
# The list of (static) values is directly given in self.validator.
res = []
for value in self.validator:
label = '%s_list_%s' % (self.labelId, value)
if withTranslations:
res.append( (value, obj.translate(label)) )
else:
res.append(value)
if withBlankValue and not self.isMultiValued():
# Create the blank value to insert at the beginning of the list
if withTranslations:
blankValue = ('', obj.translate('choose_a_value'))
else:
blankValue = ''
# Insert the blank value in the result
if isinstance(res, tuple):
res = (blankValue,) + res
else:
res.insert(0, blankValue)
return res
def validateValue(self, obj, value):
if self.format == String.CAPTCHA:
challenge = obj.REQUEST.SESSION.get('captcha', None)
# Compute the challenge minus the char to remove
i = challenge['number']-1
text = challenge['text'][:i] + challenge['text'][i+1:]
if value != text:
return obj.translate('bad_captcha')
elif self.isSelect:
# Check that the value is among possible values
possibleValues = self.getPossibleValues(obj)
if isinstance(value, basestring):
error = value not in possibleValues
else:
error = False
for v in value:
if v not in possibleValues:
error = True
break
if error: return obj.translate('bad_select_value')
accents = {'é':'e','è':'e','ê':'e','ë':'e','à':'a','â':'a','ä':'a',
'ù':'u','û':'u','ü':'u','î':'i','ï':'i','ô':'o','ö':'o',
'ç':'c', 'Ç':'C',
'Ù':'U','Û':'U','Ü':'U','Î':'I','Ï':'I','Ô':'O','Ö':'O',
'É':'E','È':'E','Ê':'E','Ë':'E','À':'A','Â':'A','Ä':'A'}
def applyTransform(self, value):
'''Applies a transform as required by self.transform on single
value p_value.'''
if self.transform in ('uppercase', 'lowercase'):
# For those transforms, I will remove any accent, because
# (1) 'é'.upper() or 'Ê'.lower() has no effect;
# (2) most of the time, if the user wants to apply such effect, it
# is for ease of data manipulation, so I guess without accent.
for c, n in self.accents.iteritems():
if c in value: value = value.replace(c, n)
# Apply the transform
if self.transform == 'lowercase': return value.lower()
elif self.transform == 'uppercase': return value.upper()
elif self.transform == 'capitalize': return value.capitalize()
return value
def getStorableValue(self, value):
isString = isinstance(value, basestring)
# Apply transform if required
if isString and not self.isEmptyValue(value) and \
(self.transform != 'none'):
value = self.applyTransform(value)
# Truncate the result if longer than self.maxChars
if isString and self.maxChars and (len(value) > self.maxChars):
value = value[:self.maxChars]
# Get a multivalued value if required.
if value and self.isMultiValued() and \
(type(value) not in sutils.sequenceTypes):
value = [value]
return value
def getIndexType(self):
'''Index type varies depending on String parameters.'''
# If String.isSelect, be it multivalued or not, we define a ListIndex:
# this way we can use AND/OR operator.
if self.isSelect:
return 'ListIndex'
elif self.format == String.TEXT:
return 'TextIndex'
elif self.format == String.XHTML:
return 'XhtmlIndex'
return Field.getIndexType(self)
def getJs(self, layoutType, res):
if self.format == String.XHTML: Field.getJs(self, layoutType, res)
def getCaptchaChallenge(self, session):
'''Returns a Captcha challenge in the form of a dict. At key "text",
value is a string that the user will be required to re-type, but
without 1 character whose position is at key "number". The challenge
is stored in the p_session, for the server-side subsequent check.'''
length = random.randint(5, 9) # The length of the challenge to encode
number = random.randint(1, length) # The position of the char to remove
text = '' # The challenge the user needs to type (minus one char)
for i in range(length):
j = random.randint(0, 1)
chars = (j == 0) and digits or letters
# Choose a char
text += chars[random.randint(0,len(chars)-1)]
res = {'text': text, 'number': number}
session['captcha'] = res
return res
def generatePassword(self):
'''Generates a password (we recycle here the captcha challenge
generator).'''
return self.getCaptchaChallenge({})['text']
# ------------------------------------------------------------------------------

File diff suppressed because it is too large Load diff

View file

@ -55,7 +55,7 @@ class ClassDescriptor(Descriptor):
except AttributeError: except AttributeError:
attrValue = getattr(self.modelClass, attrName) attrValue = getattr(self.modelClass, attrName)
hookClass = self.modelClass hookClass = self.modelClass
if isinstance(attrValue, gen.Type): if isinstance(attrValue, gen.Field):
if not condition or eval(condition): if not condition or eval(condition):
attrs.append( (attrName, attrValue, hookClass) ) attrs.append( (attrName, attrValue, hookClass) )
# Then, add attributes from parent classes # Then, add attributes from parent classes
@ -124,7 +124,7 @@ class ClassDescriptor(Descriptor):
attrValue = getattr(self.klass, attrName) attrValue = getattr(self.klass, attrName)
except AttributeError: except AttributeError:
attrValue = getattr(self.modelClass, attrName) attrValue = getattr(self.modelClass, attrName)
if not isinstance(attrValue, gen.Type): continue if not isinstance(attrValue, gen.Field): continue
FieldDescriptor(attrName, attrValue, self).generate() FieldDescriptor(attrName, attrValue, self).generate()
def isAbstract(self): def isAbstract(self):

View file

@ -158,7 +158,7 @@ class Generator:
workflow.''' workflow.'''
res = 'none' res = 'none'
for attrValue in klass.__dict__.itervalues(): for attrValue in klass.__dict__.itervalues():
if isinstance(attrValue, gen.Type): if isinstance(attrValue, gen.Field):
res = 'class' res = 'class'
elif isinstance(attrValue, gen.State): elif isinstance(attrValue, gen.State):
res = 'workflow' res = 'workflow'
@ -218,7 +218,7 @@ class Generator:
# programmatically # programmatically
moreAttrs = [] moreAttrs = []
for eName, eValue in moduleElem.__dict__.iteritems(): for eName, eValue in moduleElem.__dict__.iteritems():
if isinstance(eValue, gen.Type) and (eName not in attrs): if isinstance(eValue, gen.Field) and (eName not in attrs):
moreAttrs.append(eName) moreAttrs.append(eName)
# Sort them in alphabetical order: else, order would be random # Sort them in alphabetical order: else, order would be random
moreAttrs.sort() moreAttrs.sort()

View file

@ -385,7 +385,7 @@ class ZopeInstaller:
for baseClass in klass.wrapperClass.__bases__: for baseClass in klass.wrapperClass.__bases__:
if baseClass.__name__ == 'AbstractWrapper': continue if baseClass.__name__ == 'AbstractWrapper': continue
for name, appyType in baseClass.__dict__.iteritems(): for name, appyType in baseClass.__dict__.iteritems():
if not isinstance(appyType, gen.Type) or \ if not isinstance(appyType, gen.Field) or \
(isinstance(appyType, gen.Ref) and appyType.isBack): (isinstance(appyType, gen.Ref) and appyType.isBack):
continue # Back refs are initialised within fw refs continue # Back refs are initialised within fw refs
appyType.init(name, baseClass, appName) appyType.init(name, baseClass, appName)

View file

@ -2,7 +2,7 @@
import os, os.path, sys, re, time, random, types, base64, urllib import os, os.path, sys, re, time, random, types, base64, urllib
from appy import Object from appy import Object
import appy.gen import appy.gen
from appy.gen import Type, Search, Selection, String, Page from appy.gen import Search, String, Page
from appy.gen.utils import SomeObjects, getClassName, GroupDescr, SearchDescr from appy.gen.utils import SomeObjects, getClassName, GroupDescr, SearchDescr
from appy.gen.mixins import BaseMixin from appy.gen.mixins import BaseMixin
from appy.gen.wrappers import AbstractWrapper from appy.gen.wrappers import AbstractWrapper
@ -476,6 +476,11 @@ class ToolMixin(BaseMixin):
sub-lists of p_sub elements.''' sub-lists of p_sub elements.'''
return splitList(l, sub) return splitList(l, sub)
def quote(self, s):
'''Returns the quoted version of p_s.'''
if "'" in s: return '&quot;%s&quot;' % s
return "'%s'" % s
def getLayoutType(self): def getLayoutType(self):
'''Guess the current layout type, according to actual URL.''' '''Guess the current layout type, according to actual URL.'''
url = self.REQUEST['ACTUAL_URL'] url = self.REQUEST['ACTUAL_URL']

View file

@ -1,4 +1,4 @@
<tal:comment replace="nothing">View macro</tal:comment> <tal:comment replace="nothing">Month view macro</tal:comment>
<div metal:define-macro="viewMonth" <div metal:define-macro="viewMonth"
tal:define="fieldName request/fieldName; tal:define="fieldName request/fieldName;
ajaxHookId python: contextObj.UID() + fieldName; ajaxHookId python: contextObj.UID() + fieldName;

View file

@ -4,7 +4,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import os, os.path, mimetypes import os, os.path, mimetypes
import appy.pod import appy.pod
from appy.gen import Type, Search, Ref, String, WorkflowAnonymous from appy.gen import Field, Search, Ref, String, WorkflowAnonymous
from appy.gen.indexer import defaultIndexes from appy.gen.indexer import defaultIndexes
from appy.gen.utils import createObject from appy.gen.utils import createObject
from appy.px import Px from appy.px import Px
@ -370,7 +370,7 @@ class AbstractWrapper(object):
appUrl=app.absolute_url(); appFolder=app.data; appUrl=app.absolute_url(); appFolder=app.data;
appName=ztool.getAppName(); _=ztool.translate; appName=ztool.getAppName(); _=ztool.translate;
req=ztool.REQUEST; resp=req.RESPONSE; req=ztool.REQUEST; resp=req.RESPONSE;
lang=ztool.getUserLanguage(); lang=ztool.getUserLanguage(); q=ztool.quote;
layoutType=ztool.getLayoutType(); layoutType=ztool.getLayoutType();
contextObj=ztool.getPublishedObject(layoutType) or \ contextObj=ztool.getPublishedObject(layoutType) or \
ztool.getHomeObject(); ztool.getHomeObject();
@ -998,7 +998,7 @@ class AbstractWrapper(object):
# Now, let's try to return a real attribute. # Now, let's try to return a real attribute.
res = object.__getattribute__(self, name) res = object.__getattribute__(self, name)
# If we got an Appy type, return the value of this type for this object # If we got an Appy type, return the value of this type for this object
if isinstance(res, Type): if isinstance(res, Field):
o = self.o o = self.o
if isinstance(res, Ref): if isinstance(res, Ref):
return res.getValue(o, noListIfSingleObj=True) return res.getValue(o, noListIfSingleObj=True)

View file

@ -139,11 +139,12 @@ class Buffer:
def getLength(self): pass # To be overridden def getLength(self): pass # To be overridden
def dumpStartElement(self, elem, attrs={}, ignoreAttrs=(), hook=False, def dumpStartElement(self, elem, attrs={}, ignoreAttrs=(), hook=False,
noEndTag=False): noEndTag=False, renamedAttrs=None):
'''Inserts into this buffer the start tag p_elem, with its p_attrs, '''Inserts into this buffer the start tag p_elem, with its p_attrs,
excepted those listed in p_ignoreAttrs. If p_hook is not None excepted those listed in p_ignoreAttrs. Attrs can be dumped with an
(works only for MemoryBuffers), we will insert, at the end of the alternative name if specified in dict p_renamedAttrs. If p_hook is
list of dumped attributes: not None (works only for MemoryBuffers), we will insert, at the end
of the list of dumped attributes:
* [pod] an Attributes instance, in order to be able, when evaluating * [pod] an Attributes instance, in order to be able, when evaluating
the buffer, to dump additional attributes, not known at this the buffer, to dump additional attributes, not known at this
dump time; dump time;
@ -155,6 +156,7 @@ class Buffer:
self.write('<%s' % elem) self.write('<%s' % elem)
for name, value in attrs.items(): for name, value in attrs.items():
if ignoreAttrs and (name in ignoreAttrs): continue if ignoreAttrs and (name in ignoreAttrs): continue
if renamedAttrs and (name in renamedAttrs): name=renamedAttrs[name]
# If the value begins with ':', it is a Python expression. Else, # If the value begins with ':', it is a Python expression. Else,
# it is a static value. # it is a static value.
if not value.startswith(':'): if not value.startswith(':'):

View file

@ -47,7 +47,12 @@ class PxParser(XmlParser):
pxAttributes = ('var', 'for', 'if') pxAttributes = ('var', 'for', 'if')
# No-end tags # No-end tags
noEndTags = ('br', 'img', 'link', 'input') noEndTags = ('br', 'img', 'link', 'input')
noDumpTags = ('selected', 'checked') noDumpTags = ('selected', 'checked', 'disabled', 'multiple')
# The following dict allows to convert attrs "lfor" to "for". Indeed,
# because tags "label" can have an attribute named "for", it clashes with
# the "for" attribute added by PX. The solution is to force users to write,
# in their PX, the HTML attr for" as "lfor".
renamedAttributes = {'lfor': 'for'}
def __init__(self, env, caller=None): def __init__(self, env, caller=None):
XmlParser.__init__(self, env, caller) XmlParser.__init__(self, env, caller)
@ -86,7 +91,7 @@ class PxParser(XmlParser):
break break
e.currentBuffer.dumpStartElement(elem, attrs, e.currentBuffer.dumpStartElement(elem, attrs,
ignoreAttrs=ignorableAttrs, noEndTag=elem in self.noEndTags, ignoreAttrs=ignorableAttrs, noEndTag=elem in self.noEndTags,
hook=hook) hook=hook, renamedAttrs=self.renamedAttributes)
def endElement(self, elem): def endElement(self, elem):
e = self.env e = self.env