[gen] Moved Appy fields into appy/fields together with their PX.
This commit is contained in:
parent
2b5d286668
commit
25b4edfc1d
875
fields/__init__.py
Normal file
875
fields/__init__.py
Normal 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
114
fields/action.py
Normal 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
99
fields/boolean.py
Normal 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/>
|
||||
<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
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,17 +1,250 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import types
|
||||
from appy import Object
|
||||
from appy.gen import Type
|
||||
from appy.gen import Field
|
||||
from appy.px import Px
|
||||
from DateTime import DateTime
|
||||
from BTrees.IOBTree import IOBTree
|
||||
from persistent.list import PersistentList
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Calendar(Type):
|
||||
class Calendar(Field):
|
||||
'''This field allows to produce an agenda (monthly view) and view/edit
|
||||
events on it.'''
|
||||
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) < \
|
||||
grid[0][0].strftime(fmt));
|
||||
goForward=not endDate or (endDate.strftime(fmt) > \
|
||||
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 < startDate);
|
||||
tooLate=endDate and not tooEarly and (date > 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,
|
||||
default=None, show='view', page='main', group=None,
|
||||
layouts=None, move=0, specificReadPermission=False,
|
||||
|
@ -21,7 +254,7 @@ class Calendar(Type):
|
|||
otherCalendars=None, additionalInfo=None, startDate=None,
|
||||
endDate=None, defaultDate=None, preCompute=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,
|
||||
specificWritePermission, width, height, None, colspan,
|
||||
master, masterValue, focus, False, True, mapping, label,
|
118
fields/computed.py
Normal file
118
fields/computed.py
Normal 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/>
|
||||
<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
251
fields/date.py
Normal 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"> </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> </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
226
fields/file.py
Normal 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> -
|
||||
<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
105
fields/float.py
Normal 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/>
|
||||
<!-- 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
39
fields/info.py
Normal 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
80
fields/integer.py
Normal 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/>
|
||||
<!-- 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
166
fields/list.py
Normal 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)
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,7 +1,8 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import sha
|
||||
from appy import Object
|
||||
from appy.gen import Type
|
||||
from appy.gen import Field
|
||||
from appy.px import Px
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class OgoneConfig:
|
||||
|
@ -25,17 +26,36 @@ class OgoneConfig:
|
|||
def __repr__(self): return str(self.__dict__)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Ogone(Type):
|
||||
class Ogone(Field):
|
||||
'''This field allows to perform payments with the Ogone (r) system.'''
|
||||
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',
|
||||
group=None, layouts=None, move=0, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=None,
|
||||
colspan=1, master=None, masterValue=None, focus=False,
|
||||
mapping=None, label=None):
|
||||
Type.__init__(self, None, (0,1), None, show, page, group, layouts, move,
|
||||
False, False,specificReadPermission,
|
||||
Field.__init__(self, None, (0,1), None, show, page, group, layouts,
|
||||
move, False, False,specificReadPermission,
|
||||
specificWritePermission, width, height, None, colspan,
|
||||
master, masterValue, focus, False, True, mapping, label,
|
||||
None, None, None, None)
|
226
fields/pod.py
Normal file
226
fields/pod.py
Normal 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
672
fields/ref.py
Normal 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)>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 > 0" style="cursor:pointer"
|
||||
src=":'%s/ui/arrowUp.png' % $appUrl" title=":_('move_up')"
|
||||
onclick=":ajaxBaseCall.replace('**v**', 'up')"/><img
|
||||
style="cursor:pointer" if="objectIndex < (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&className=%s&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)<=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&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/>
|
||||
<!-- 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
506
fields/string.py
Normal 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']
|
||||
# ------------------------------------------------------------------------------
|
2496
gen/__init__.py
2496
gen/__init__.py
File diff suppressed because it is too large
Load diff
|
@ -55,7 +55,7 @@ class ClassDescriptor(Descriptor):
|
|||
except AttributeError:
|
||||
attrValue = getattr(self.modelClass, attrName)
|
||||
hookClass = self.modelClass
|
||||
if isinstance(attrValue, gen.Type):
|
||||
if isinstance(attrValue, gen.Field):
|
||||
if not condition or eval(condition):
|
||||
attrs.append( (attrName, attrValue, hookClass) )
|
||||
# Then, add attributes from parent classes
|
||||
|
@ -124,7 +124,7 @@ class ClassDescriptor(Descriptor):
|
|||
attrValue = getattr(self.klass, attrName)
|
||||
except AttributeError:
|
||||
attrValue = getattr(self.modelClass, attrName)
|
||||
if not isinstance(attrValue, gen.Type): continue
|
||||
if not isinstance(attrValue, gen.Field): continue
|
||||
FieldDescriptor(attrName, attrValue, self).generate()
|
||||
|
||||
def isAbstract(self):
|
||||
|
|
|
@ -158,7 +158,7 @@ class Generator:
|
|||
workflow.'''
|
||||
res = 'none'
|
||||
for attrValue in klass.__dict__.itervalues():
|
||||
if isinstance(attrValue, gen.Type):
|
||||
if isinstance(attrValue, gen.Field):
|
||||
res = 'class'
|
||||
elif isinstance(attrValue, gen.State):
|
||||
res = 'workflow'
|
||||
|
@ -218,7 +218,7 @@ class Generator:
|
|||
# programmatically
|
||||
moreAttrs = []
|
||||
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)
|
||||
# Sort them in alphabetical order: else, order would be random
|
||||
moreAttrs.sort()
|
||||
|
|
|
@ -385,7 +385,7 @@ class ZopeInstaller:
|
|||
for baseClass in klass.wrapperClass.__bases__:
|
||||
if baseClass.__name__ == 'AbstractWrapper': continue
|
||||
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):
|
||||
continue # Back refs are initialised within fw refs
|
||||
appyType.init(name, baseClass, appName)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import os, os.path, sys, re, time, random, types, base64, urllib
|
||||
from appy import Object
|
||||
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.mixins import BaseMixin
|
||||
from appy.gen.wrappers import AbstractWrapper
|
||||
|
@ -476,6 +476,11 @@ class ToolMixin(BaseMixin):
|
|||
sub-lists of p_sub elements.'''
|
||||
return splitList(l, sub)
|
||||
|
||||
def quote(self, s):
|
||||
'''Returns the quoted version of p_s.'''
|
||||
if "'" in s: return '"%s"' % s
|
||||
return "'%s'" % s
|
||||
|
||||
def getLayoutType(self):
|
||||
'''Guess the current layout type, according to actual URL.'''
|
||||
url = self.REQUEST['ACTUAL_URL']
|
||||
|
|
|
@ -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"
|
||||
tal:define="fieldName request/fieldName;
|
||||
ajaxHookId python: contextObj.UID() + fieldName;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import os, os.path, mimetypes
|
||||
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.utils import createObject
|
||||
from appy.px import Px
|
||||
|
@ -370,7 +370,7 @@ class AbstractWrapper(object):
|
|||
appUrl=app.absolute_url(); appFolder=app.data;
|
||||
appName=ztool.getAppName(); _=ztool.translate;
|
||||
req=ztool.REQUEST; resp=req.RESPONSE;
|
||||
lang=ztool.getUserLanguage();
|
||||
lang=ztool.getUserLanguage(); q=ztool.quote;
|
||||
layoutType=ztool.getLayoutType();
|
||||
contextObj=ztool.getPublishedObject(layoutType) or \
|
||||
ztool.getHomeObject();
|
||||
|
@ -998,7 +998,7 @@ class AbstractWrapper(object):
|
|||
# Now, let's try to return a real attribute.
|
||||
res = object.__getattribute__(self, name)
|
||||
# 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
|
||||
if isinstance(res, Ref):
|
||||
return res.getValue(o, noListIfSingleObj=True)
|
||||
|
|
|
@ -139,11 +139,12 @@ class Buffer:
|
|||
def getLength(self): pass # To be overridden
|
||||
|
||||
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,
|
||||
excepted those listed in p_ignoreAttrs. If p_hook is not None
|
||||
(works only for MemoryBuffers), we will insert, at the end of the
|
||||
list of dumped attributes:
|
||||
excepted those listed in p_ignoreAttrs. Attrs can be dumped with an
|
||||
alternative name if specified in dict p_renamedAttrs. If p_hook is
|
||||
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
|
||||
the buffer, to dump additional attributes, not known at this
|
||||
dump time;
|
||||
|
@ -155,6 +156,7 @@ class Buffer:
|
|||
self.write('<%s' % elem)
|
||||
for name, value in attrs.items():
|
||||
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,
|
||||
# it is a static value.
|
||||
if not value.startswith(':'):
|
||||
|
|
|
@ -47,7 +47,12 @@ class PxParser(XmlParser):
|
|||
pxAttributes = ('var', 'for', 'if')
|
||||
# No-end tags
|
||||
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):
|
||||
XmlParser.__init__(self, env, caller)
|
||||
|
@ -86,7 +91,7 @@ class PxParser(XmlParser):
|
|||
break
|
||||
e.currentBuffer.dumpStartElement(elem, attrs,
|
||||
ignoreAttrs=ignorableAttrs, noEndTag=elem in self.noEndTags,
|
||||
hook=hook)
|
||||
hook=hook, renamedAttrs=self.renamedAttributes)
|
||||
|
||||
def endElement(self, elem):
|
||||
e = self.env
|
||||
|
|
Loading…
Reference in a new issue