2226 lines
102 KiB
Python
2226 lines
102 KiB
Python
# -*- coding: utf-8 -*-
|
|
# ------------------------------------------------------------------------------
|
|
import re, time, copy, sys, types, os, os.path, mimetypes
|
|
from appy.shared.utils import Traceback
|
|
from appy.gen.layout import Table
|
|
from appy.gen.layout import defaultFieldLayouts
|
|
from appy.gen.po import PoMessage
|
|
from appy.gen.utils import sequenceTypes, GroupDescr, Keywords, FileWrapper, \
|
|
getClassName, SomeObjects
|
|
from appy.shared.data import languages
|
|
|
|
# Default Appy permissions -----------------------------------------------------
|
|
r, w, d = ('read', 'write', 'delete')
|
|
digit = re.compile('[0-9]')
|
|
alpha = re.compile('[a-zA-Z0-9]')
|
|
letter = re.compile('[a-zA-Z]')
|
|
nullValues = (None, '', ' ')
|
|
validatorTypes = (types.FunctionType, types.UnboundMethodType,
|
|
type(re.compile('')))
|
|
emptyTuple = ()
|
|
|
|
# Descriptor classes used for refining descriptions of elements in types
|
|
# (pages, groups,...) ----------------------------------------------------------
|
|
class Page:
|
|
'''Used for describing a page, its related phase, show condition, etc.'''
|
|
subElements = ('save', 'cancel', 'previous', 'next')
|
|
def __init__(self, name, phase='main', show=True, showSave=True,
|
|
showCancel=True, showPrevious=True, showNext=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
|
|
|
|
@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 (button "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
|
|
|
|
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):
|
|
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%.
|
|
self.wide = 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
|
|
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 = None
|
|
self.masterValue = None
|
|
if master:
|
|
self._addMaster(master, masterValue)
|
|
|
|
def _addMaster(self, master, masterValue):
|
|
'''Specifies this group being a slave of another field: we will add css
|
|
classes allowing to show/hide, in Javascript, its widget according
|
|
to master value.'''
|
|
self.master = master
|
|
self.masterValue = masterValue
|
|
classes = 'slave_%s' % self.master.id
|
|
if type(self.masterValue) not in sequenceTypes:
|
|
masterValues = [self.masterValue]
|
|
else:
|
|
masterValues = self.masterValue
|
|
for masterValue in masterValues:
|
|
classes += ' slaveValue_%s_%s' % (self.master.id, masterValue)
|
|
self.css_class += ' ' + classes
|
|
|
|
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:
|
|
# We have case (a)
|
|
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):
|
|
'''This method allows to generate all the needed i18n labels related to
|
|
this group. p_messages is the list of i18n p_messages that we are
|
|
currently building; p_classDescr is the descriptor of the class where
|
|
this group is defined.'''
|
|
if self.hasLabel:
|
|
msgId = '%s_group_%s' % (classDescr.name, self.name)
|
|
poMsg = PoMessage(msgId, '', self.name, niceDefault=True)
|
|
if poMsg not in messages:
|
|
messages.append(poMsg)
|
|
classDescr.labelsToPropagate.append(poMsg)
|
|
if self.hasDescr:
|
|
msgId = '%s_group_%s_descr' % (classDescr.name, self.name)
|
|
poMsg = PoMessage(msgId, '', ' ')
|
|
if poMsg not in messages:
|
|
messages.append(poMsg)
|
|
classDescr.labelsToPropagate.append(poMsg)
|
|
if self.hasHelp:
|
|
msgId = '%s_group_%s_help' % (classDescr.name, self.name)
|
|
poMsg = PoMessage(msgId, '', ' ')
|
|
if poMsg not in messages:
|
|
messages.append(poMsg)
|
|
classDescr.labelsToPropagate.append(poMsg)
|
|
if self.hasHeaders:
|
|
for i in range(self.nbOfHeaders):
|
|
msgId = '%s_group_%s_col%d' % (classDescr.name, self.name, i+1)
|
|
poMsg = PoMessage(msgId, '', ' ')
|
|
if poMsg not in messages:
|
|
messages.append(poMsg)
|
|
classDescr.labelsToPropagate.append(poMsg)
|
|
walkedGroups.add(self)
|
|
if self.group and (self.group not in walkedGroups):
|
|
# We remember walked groups for avoiding infinite recursion.
|
|
self.group.generateLabels(messages, classDescr, walkedGroups)
|
|
|
|
def insertInto(self, widgets, groupDescrs, page, metaType):
|
|
'''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, 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] = GroupDescr(self, page,
|
|
metaType).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)
|
|
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
|
|
|
|
class Import:
|
|
'''Used for describing the place where to find the data to use for creating
|
|
an object.'''
|
|
def __init__(self, path, onElement=None, headers=(), sort=None):
|
|
self.id = 'import'
|
|
self.path = path
|
|
# p_onElement hereafter must be a function (or a static method) that
|
|
# will be called every time an element to import is found. It takes a
|
|
# single arg that is the absolute filen name of the file to import,
|
|
# within p_path. It must return a list of info about the element, or
|
|
# None if the element must be ignored. The list will be used to display
|
|
# information about the element in a tabular form.
|
|
self.onElement = onElement
|
|
# The following attribute must contain the names of the column headers
|
|
# of the table that will display elements to import (retrieved from
|
|
# calls to self.onElement). Every not-None element retrieved from
|
|
# self.onElement must have the same length as self.headers.
|
|
self.headers = headers
|
|
# The following attribute must store a function or static method that
|
|
# will be used to sort elements to import. It will be called with a
|
|
# single param containing the list of all not-None elements as retrieved
|
|
# by calls to self.onElement (but with one additional first element in
|
|
# every list, which is the absolute file name of the element to import)
|
|
# and must return a similar, sorted, list.
|
|
self.sort = sort
|
|
|
|
class Search:
|
|
'''Used for specifying a search for a given type.'''
|
|
def __init__(self, name, group=None, sortBy='', limit=None, **fields):
|
|
self.name = name
|
|
self.group = group # Searches may be visually grouped in the portlet
|
|
self.sortBy = sortBy
|
|
self.limit = limit
|
|
# In the dict below, keys are indexed field names and values are
|
|
# search values.
|
|
self.fields = fields
|
|
@staticmethod
|
|
def getIndexName(fieldName, usage='search'):
|
|
'''Gets the name of the technical index that corresponds to field named
|
|
p_fieldName. Indexes can be used for searching (p_usage="search") or
|
|
for sorting (usage="sort"). The method returns None if the field
|
|
named p_fieldName can't be used for p_usage.'''
|
|
if fieldName == 'title':
|
|
if usage == 'search': return 'Title'
|
|
else: return 'sortable_title'
|
|
# Indeed, for field 'title', Plone has created a specific index
|
|
# 'sortable_title', because index 'Title' is a ZCTextIndex
|
|
# (for searchability) and can't be used for sorting.
|
|
elif fieldName == 'description':
|
|
if usage == 'search': return 'Description'
|
|
else: return None
|
|
elif fieldName == 'state': return 'review_state'
|
|
else:
|
|
return 'get%s%s'% (fieldName[0].upper(),fieldName[1:])
|
|
@staticmethod
|
|
def getSearchValue(fieldName, fieldValue):
|
|
'''Returns a transformed p_fieldValue for producing a valid search
|
|
value as required for searching in the index corresponding to
|
|
p_fieldName.'''
|
|
if fieldName == 'title':
|
|
# Title is a ZCTextIndex. We must split p_fieldValue into keywords.
|
|
res = Keywords(fieldValue.decode('utf-8')).get()
|
|
elif isinstance(fieldValue, basestring) and fieldValue.endswith('*'):
|
|
v = fieldValue[:-1]
|
|
# Warning: 'z' is higher than 'Z'!
|
|
res = {'query':(v,v+'z'), 'range':'min:max'}
|
|
elif type(fieldValue) in sequenceTypes:
|
|
if fieldValue and isinstance(fieldValue[0], basestring):
|
|
# We have a list of string values (ie: we need to
|
|
# search v1 or v2 or...)
|
|
res = fieldValue
|
|
else:
|
|
# We have a range of (int, float, DateTime...) values
|
|
minv, maxv = fieldValue
|
|
rangev = 'minmax'
|
|
queryv = fieldValue
|
|
if minv == None:
|
|
rangev = 'max'
|
|
queryv = maxv
|
|
elif maxv == None:
|
|
rangev = 'min'
|
|
queryv = minv
|
|
res = {'query':queryv, 'range':rangev}
|
|
else:
|
|
res = fieldValue
|
|
return res
|
|
|
|
# ------------------------------------------------------------------------------
|
|
class Type:
|
|
'''Basic abstract class for defining any appy type.'''
|
|
def __init__(self, validator, multiplicity, index, default, optional,
|
|
editDefault, show, page, group, layouts, move, indexed,
|
|
searchable, specificReadPermission, specificWritePermission,
|
|
width, height, colspan, master, masterValue, focus,
|
|
historized, sync):
|
|
# 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 tuple indicating the minimum and maximum
|
|
# occurrences of values.
|
|
self.multiplicity = multiplicity
|
|
# Type of the index on the values. If you want to represent a simple
|
|
# (ordered) list of values, specify None. If you want to
|
|
# index your values with unordered integers or with other types like
|
|
# strings (thus creating a dictionary of values instead of a list),
|
|
# specify a type specification for the index, like Integer() or
|
|
# String(). Note that this concept of "index" has nothing to do with
|
|
# the concept of "database index" (see fields "indexed" and
|
|
# "searchable" below). self.index is not yet used.
|
|
self.index = index
|
|
# Default value
|
|
self.default = default
|
|
# Is the field optional or not ?
|
|
self.optional = optional
|
|
# Is the field required or not ? (derived from multiplicity)
|
|
self.required = self.multiplicity[0] > 0
|
|
# May the user configure a default value ?
|
|
self.editDefault = editDefault
|
|
# 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 field value
|
|
# appear?
|
|
self.group = Group.get(group)
|
|
# The following attribute allows to move a field back to a previous
|
|
# position (useful for content types that inherit from others).
|
|
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
|
|
# 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 behaviour of this field may depend on another, "master" field
|
|
self.master = master
|
|
self.slaves = [] # The list of slaves of this field, if it is a master
|
|
# Every HTML input field corresponding to a master must get some
|
|
# CSS classes for controlling its slaves.
|
|
self.master_css = ''
|
|
if master:
|
|
self.master.slaves.append(self)
|
|
self.master.master_css = 'appyMaster master_%s' % self.master.id
|
|
# When master has some value(s), there is impact on this field.
|
|
self.masterValue = 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)
|
|
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
|
|
|
|
def init(self, name, klass, appName):
|
|
'''When the application server starts, this secondary constructor is
|
|
called for storing the names 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).'''
|
|
self.name = name
|
|
# Determine ids of i18n labels for this field
|
|
if not klass: prefix = appName
|
|
else: prefix = getClassName(klass, appName)
|
|
self.labelId = '%s_%s' % (prefix, name)
|
|
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 isinstance(self, Ref):
|
|
self.backd = self.back.__dict__
|
|
if isinstance(self, Ref) and not self.isBack:
|
|
self.back.relationship = '%s_%s_rel' % (prefix, name)
|
|
|
|
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
|
|
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?'''
|
|
# Do not show field if it is optional and not selected in tool
|
|
if self.optional:
|
|
tool = obj.getTool().appy()
|
|
fieldName = 'optionalFieldsFor%s' % obj.meta_type
|
|
fieldValue = getattr(tool, fieldName, ())
|
|
if self.name not in fieldValue:
|
|
return False
|
|
# Check if the user has the permission to view or edit the field
|
|
user = obj.portal_membership.getAuthenticatedMember()
|
|
if layoutType == 'edit':
|
|
perm = self.writePermission
|
|
else:
|
|
perm = self.readPermission
|
|
if not user.has_permission(perm, obj):
|
|
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', 'search'...
|
|
if 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 = master.getStorableValue(reqValue)
|
|
# Manage the fact that values can be lists or single values
|
|
multiMaster = type(masterValue) in sequenceTypes
|
|
multiReq = type(reqValue) in sequenceTypes
|
|
if not multiMaster and not multiReq: return reqValue == masterValue
|
|
elif multiMaster and not multiReq: return reqValue in masterValue
|
|
elif not multiMaster and multiReq: return masterValue in reqValue
|
|
else: # multiMaster and multiReq
|
|
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 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('no-style-table')
|
|
if self.master:
|
|
# This type has a master (so is a slave): we add css classes
|
|
# allowing to show/hide, in Javascript, its widget according to
|
|
# master value.
|
|
classes = 'slave_%s' % self.master.id
|
|
if type(self.masterValue) not in sequenceTypes:
|
|
masterValues = [self.masterValue]
|
|
else:
|
|
masterValues = self.masterValue
|
|
for masterValue in masterValues:
|
|
classes += ' slaveValue_%s_%s' % (self.master.id, masterValue)
|
|
layouts['view'].addCssClasses(classes)
|
|
layouts['edit'].addCssClasses(classes)
|
|
if self.focus:
|
|
# We need to make it flashy
|
|
layouts['view'].addCssClasses('appyFocus')
|
|
layouts['edit'].addCssClasses('appyFocus')
|
|
# If layouts are the default ones, set width=None instead of width=100%
|
|
# for the field if it is not in a group.
|
|
if areDefault and not self.group:
|
|
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 Type 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):
|
|
'''This method returns a list of CSS files that are required for
|
|
displaying widgets of self's type on a given p_layoutType.'''
|
|
|
|
def getJs(self, layoutType):
|
|
'''This method returns a list of Javascript files that are required for
|
|
displaying widgets of self's type on a given p_layoutType.'''
|
|
|
|
def getValue(self, obj):
|
|
'''Gets, on_obj, the value conforming to self's type definition.'''
|
|
value = getattr(obj, self.name, None)
|
|
if (value == None):
|
|
# If there is no value, get the default value if any
|
|
if not self.editDefault:
|
|
# Return self.default, of self.default() if it is a method
|
|
if callable(self.default):
|
|
try:
|
|
return self.callMethod(obj, self.default,
|
|
raiseOnError=True)
|
|
except Exception, e:
|
|
# Already logged.
|
|
return None
|
|
else:
|
|
return self.default
|
|
# If value is editable, get the default value from the tool
|
|
portalTypeName = obj._appy_getPortalType(obj.REQUEST)
|
|
tool = obj.getTool().appy()
|
|
return getattr(tool, 'defaultValueFor%s' % self.labelId)
|
|
return value
|
|
|
|
def getFormattedValue(self, obj, value):
|
|
'''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 self.isEmptyValue(value): return ''
|
|
return value
|
|
|
|
def getIndexType(self):
|
|
'''Returns the name of the technical, Zope-level index type for this
|
|
field.'''
|
|
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 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):
|
|
'''Gets the string (or list of strings if multi-valued)
|
|
representation of this field as found in the p_request.'''
|
|
return request.get(self.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 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
|
|
# 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 clone(self, forTool=True):
|
|
'''Returns a clone of myself. If p_forTool is True, the clone will be
|
|
adapted to its life into the tool.'''
|
|
res = copy.copy(self)
|
|
res.group = copy.copy(self.group)
|
|
res.page = copy.copy(self.page)
|
|
if not forTool: return res
|
|
# A field added to the tool can't have parameters that would lead to the
|
|
# creation of new fields in the tool.
|
|
res.editDefault = False
|
|
res.optional = False
|
|
res.show = True
|
|
# Set default layouts for all Tool fields
|
|
res.layouts = res.formatLayouts(None)
|
|
res.specificReadPermission = False
|
|
res.specificWritePermission = False
|
|
res.multiplicity = (0, self.multiplicity[1])
|
|
if callable(res.validator):
|
|
# We will not be able to call this function from the tool.
|
|
res.validator = None
|
|
return res
|
|
|
|
def callMethod(self, obj, method, raiseOnError=False):
|
|
'''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 method(obj)
|
|
except TypeError, te:
|
|
# Try a version of the method that would accept self as an
|
|
# additional parameter.
|
|
try:
|
|
return method(obj, self)
|
|
except Exception, e:
|
|
obj.log(Traceback.get(), type='error')
|
|
if raiseOnError: raise e
|
|
else: return str(e)
|
|
except Exception, e:
|
|
obj.log(Traceback.get(), type='error')
|
|
if raiseOnError: raise e
|
|
else: return str(e)
|
|
|
|
class Integer(Type):
|
|
def __init__(self, validator=None, multiplicity=(0,1), index=None,
|
|
default=None, optional=False, editDefault=False, show=True,
|
|
page='main', group=None, layouts=None, move=0, indexed=False,
|
|
searchable=False, specificReadPermission=False,
|
|
specificWritePermission=False, width=6, height=None,
|
|
colspan=1, master=None, masterValue=None, focus=False,
|
|
historized=False):
|
|
Type.__init__(self, validator, multiplicity, index, default, optional,
|
|
editDefault, show, page, group, layouts, move, indexed,
|
|
searchable, specificReadPermission,
|
|
specificWritePermission, width, height, colspan, master,
|
|
masterValue, focus, historized, True)
|
|
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):
|
|
if self.isEmptyValue(value): return ''
|
|
return str(value)
|
|
|
|
class Float(Type):
|
|
allowedDecimalSeps = (',', '.')
|
|
def __init__(self, validator=None, multiplicity=(0,1), index=None,
|
|
default=None, optional=False, editDefault=False, show=True,
|
|
page='main', group=None, layouts=None, move=0, indexed=False,
|
|
searchable=False, specificReadPermission=False,
|
|
specificWritePermission=False, width=6, height=None,
|
|
colspan=1, master=None, masterValue=None, focus=False,
|
|
historized=False, precision=None, sep=(',', '.')):
|
|
# 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 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 'Char "%s" is not allowed as decimal separator.' % sep
|
|
Type.__init__(self, validator, multiplicity, index, default, optional,
|
|
editDefault, show, page, group, layouts, move, indexed,
|
|
False, specificReadPermission, specificWritePermission,
|
|
width, height, colspan, master, masterValue, focus,
|
|
historized, True)
|
|
self.pythonType = float
|
|
|
|
def getFormattedValue(self, obj, value):
|
|
if self.isEmptyValue(value): return ''
|
|
# Determine the field separator
|
|
sep = self.sep[0]
|
|
# Produce the rounded string representation
|
|
if self.precision == None:
|
|
res = str(value)
|
|
else:
|
|
format = '%%.%df' % self.precision
|
|
res = format % value
|
|
# Use the correct decimal separator
|
|
res = res.replace('.', sep)
|
|
# Remove the decimal part if = 0
|
|
splitted = res.split(sep)
|
|
if len(splitted) > 1:
|
|
try:
|
|
decPart = int(splitted[1])
|
|
if decPart == 0:
|
|
res = splitted[0]
|
|
except ValueError:
|
|
# This exception may occur when the float value has an "exp"
|
|
# part, like in this example: 4.345e-05.
|
|
pass
|
|
return res
|
|
|
|
def validateValue(self, obj, value):
|
|
# Replace used separator with the Python separator '.'
|
|
for sep in self.sep: value = value.replace(sep, '.')
|
|
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, '.')
|
|
return self.pythonType(value)
|
|
|
|
class String(Type):
|
|
# 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 # Plone calls me erroneously for
|
|
# non-mandatory fields.
|
|
# 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 # Plone calls me erroneously for
|
|
# non-mandatory fields.
|
|
# 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 languages.exists(v[:2].lower()): 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 # Plone calls me erroneously for
|
|
# non-mandatory fields.
|
|
# 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 languages.exists(value[4:6].lower()): 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
|
|
def __init__(self, validator=None, multiplicity=(0,1), index=None,
|
|
default=None, optional=False, editDefault=False, format=LINE,
|
|
show=True, page='main', group=None, layouts=None, move=0,
|
|
indexed=False, searchable=False, specificReadPermission=False,
|
|
specificWritePermission=False, width=None, height=None,
|
|
colspan=1, master=None, masterValue=None, focus=False,
|
|
historized=False, transform='none'):
|
|
self.format = format
|
|
# 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
|
|
Type.__init__(self, validator, multiplicity, index, default, optional,
|
|
editDefault, show, page, group, layouts, move, indexed,
|
|
searchable, specificReadPermission,
|
|
specificWritePermission, width, height, colspan, master,
|
|
masterValue, focus, historized, True)
|
|
self.isSelect = self.isSelection()
|
|
# Default width and height vary according to String format
|
|
if width == None:
|
|
if format == String.TEXT: self.width = 60
|
|
else: self.width = 30
|
|
if height == None:
|
|
if format == String.TEXT: self.height = 5
|
|
elif self.isSelect: self.height = 4
|
|
else: self.height = 1
|
|
self.filterable = self.indexed and (self.format == String.LINE) and \
|
|
not self.isSelect
|
|
|
|
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 or multiplicity.'''
|
|
if self.format in (String.TEXT, String.XHTML):
|
|
return {'view': 'l-d-f', 'edit': 'lrv-d-f'}
|
|
elif self.isMultiValued():
|
|
return {'view': 'l-f', 'edit': 'lrv-f'}
|
|
|
|
def getValue(self, obj):
|
|
value = Type.getValue(self, obj)
|
|
if not value:
|
|
if self.isMultiValued(): return emptyTuple
|
|
else: return value
|
|
if isinstance(value, basestring) and self.isMultiValued():
|
|
value = [value]
|
|
# Some backward compatibilities with Archetypes.
|
|
elif value.__class__.__name__ == 'BaseUnit':
|
|
try:
|
|
value = unicode(value)
|
|
except UnicodeDecodeError:
|
|
value = str(value)
|
|
elif isinstance(value, tuple):
|
|
# When Appy storage was based on Archetype, multivalued string
|
|
# fields stored values as tuples of unicode strings.
|
|
value = list(value)
|
|
return value
|
|
|
|
def getFormattedValue(self, obj, value):
|
|
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 not isinstance(value, basestring):
|
|
# Archetypes "Description" fields may hold a BaseUnit instance.
|
|
try:
|
|
res = unicode(value)
|
|
except UnicodeDecodeError:
|
|
res = str(value)
|
|
# 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 = ('',)
|
|
def getIndexValue(self, obj, forSearch=False):
|
|
'''For indexing purposes, we return only strings, not unicodes.'''
|
|
res = Type.getIndexValue(self, obj, forSearch)
|
|
if isinstance(res, unicode):
|
|
res = res.encode('utf-8')
|
|
# Ugly portal_catalog: if I give an empty tuple as index value,
|
|
# portal_catalog 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
|
|
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 not self.isSelect: return
|
|
# 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):
|
|
if not self.isEmptyValue(value) and (self.transform != 'none'):
|
|
if isinstance(value, basestring):
|
|
return self.applyTransform(value)
|
|
else:
|
|
return [self.applyTransform(v) for v in value]
|
|
return value
|
|
|
|
def store(self, obj, value):
|
|
if self.isMultiValued() and isinstance(value, basestring):
|
|
value = [value]
|
|
exec 'obj.%s = value' % self.name
|
|
|
|
def getIndexType(self):
|
|
'''Index type varies depending on String parameters.'''
|
|
# If String.isSelect, be it multivalued or not, we define a ZCTextIndex:
|
|
# this way we can use AND/OR operator.
|
|
if self.isSelect or (self.format in (String.TEXT, String.XHTML)):
|
|
return 'ZCTextIndex'
|
|
return Type.getIndexType(self)
|
|
|
|
class Boolean(Type):
|
|
def __init__(self, validator=None, multiplicity=(0,1), index=None,
|
|
default=None, optional=False, editDefault=False, show=True,
|
|
page='main', group=None, layouts = None, move=0, indexed=False,
|
|
searchable=False, specificReadPermission=False,
|
|
specificWritePermission=False, width=None, height=None,
|
|
colspan=1, master=None, masterValue=None, focus=False,
|
|
historized=False):
|
|
Type.__init__(self, validator, multiplicity, index, default, optional,
|
|
editDefault, show, page, group, layouts, move, indexed,
|
|
searchable, specificReadPermission,
|
|
specificWritePermission, width, height, colspan, master,
|
|
masterValue, focus, historized, True)
|
|
self.pythonType = bool
|
|
|
|
def getDefaultLayouts(self):
|
|
return {'view': 'l;f!_', '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 = Type.getValue(self, obj)
|
|
if value == None: return False
|
|
return value
|
|
|
|
def getFormattedValue(self, obj, value):
|
|
if value: res = obj.translate('yes', domain='plone')
|
|
else: res = obj.translate('no', domain='plone')
|
|
return res
|
|
|
|
def getStorableValue(self, value):
|
|
if not self.isEmptyValue(value):
|
|
exec 'res = %s' % value
|
|
return res
|
|
|
|
class Date(Type):
|
|
# 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), index=None,
|
|
default=None, optional=False, editDefault=False,
|
|
format=WITH_HOUR, calendar=True,
|
|
startYear=time.localtime()[0]-10,
|
|
endYear=time.localtime()[0]+10, show=True, page='main',
|
|
group=None, layouts=None, move=0, indexed=False,
|
|
searchable=False, specificReadPermission=False,
|
|
specificWritePermission=False, width=None, height=None,
|
|
colspan=1, master=None, masterValue=None, focus=False,
|
|
historized=False):
|
|
self.format = format
|
|
self.calendar = calendar
|
|
self.startYear = startYear
|
|
self.endYear = endYear
|
|
Type.__init__(self, validator, multiplicity, index, default, optional,
|
|
editDefault, show, page, group, layouts, move, indexed,
|
|
searchable, specificReadPermission,
|
|
specificWritePermission, width, height, colspan, master,
|
|
masterValue, focus, historized, True)
|
|
|
|
def getCss(self, layoutType):
|
|
if (layoutType == 'edit') and self.calendar:
|
|
return ('jscalendar/calendar-system.css',)
|
|
|
|
def getJs(self, layoutType):
|
|
if (layoutType == 'edit') and self.calendar:
|
|
return ('jscalendar/calendar_stripped.js',
|
|
'jscalendar/calendar-en.js')
|
|
|
|
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):
|
|
if self.isEmptyValue(value): return ''
|
|
res = value.strftime('%d/%m/') + str(value.year())
|
|
if self.format == Date.WITH_HOUR:
|
|
res += ' %s' % value.strftime('%H:%M')
|
|
return res
|
|
|
|
def getRequestValue(self, request):
|
|
# Manage the "date" part
|
|
value = ''
|
|
for part in self.dateParts:
|
|
valuePart = request.get('%s_%s' % (self.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' % (self.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)
|
|
|
|
class File(Type):
|
|
def __init__(self, validator=None, multiplicity=(0,1), index=None,
|
|
default=None, optional=False, editDefault=False, show=True,
|
|
page='main', group=None, layouts=None, move=0, indexed=False,
|
|
searchable=False, specificReadPermission=False,
|
|
specificWritePermission=False, width=None, height=None,
|
|
colspan=1, master=None, masterValue=None, focus=False,
|
|
historized=False, isImage=False):
|
|
self.isImage = isImage
|
|
Type.__init__(self, validator, multiplicity, index, default, optional,
|
|
editDefault, show, page, group, layouts, move, indexed,
|
|
False, specificReadPermission, specificWritePermission,
|
|
width, height, colspan, master, masterValue, focus,
|
|
historized, True)
|
|
|
|
def getValue(self, obj):
|
|
value = Type.getValue(self, obj)
|
|
if value: value = FileWrapper(value)
|
|
return value
|
|
|
|
def getFormattedValue(self, obj, value):
|
|
if not value: return value
|
|
return value._atFile
|
|
|
|
def getRequestValue(self, request):
|
|
res = request.get('%s_file' % self.name)
|
|
return request.get('%s_file' % self.name)
|
|
|
|
def getDefaultLayouts(self): return {'view':'lf','edit':'lrv-f'}
|
|
|
|
def isEmptyValue(self, value, obj=None):
|
|
'''Must p_value be considered as empty?'''
|
|
if not obj: return Type.isEmptyValue(self, value)
|
|
if value is not None: 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.gen.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, 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, FileWrapper):
|
|
setattr(obj, self.name, value._atFile)
|
|
elif isinstance(value, basestring):
|
|
f = file(value)
|
|
fileName = os.path.basename(value)
|
|
fileId = 'file.%f' % time.time()
|
|
zopeFile = OFSImageFile(fileId, fileName, f)
|
|
zopeFile.filename = fileName
|
|
zopeFile.content_type = mimetypes.guess_type(fileName)[0]
|
|
setattr(obj, self.name, zopeFile)
|
|
f.close()
|
|
elif type(value) in 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 = obj.REQUEST.get('%s_delete' % self.name, None)
|
|
if action == 'nochange': pass
|
|
else: setattr(obj, self.name, None)
|
|
|
|
class Ref(Type):
|
|
def __init__(self, klass=None, attribute=None, validator=None,
|
|
multiplicity=(0,1), index=None, default=None, optional=False,
|
|
editDefault=False, add=False, addConfirm=False, noForm=False,
|
|
link=True, unlink=False, 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,
|
|
colspan=1, master=None, masterValue=None, focus=False,
|
|
historized=False, queryable=False, queryFields=None,
|
|
queryNbCols=1):
|
|
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
|
|
# 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 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__
|
|
# 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 = shownInfo
|
|
# 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
|
|
Type.__init__(self, validator, multiplicity, index, default, optional,
|
|
editDefault, show, page, group, layouts, move, indexed,
|
|
False, specificReadPermission, specificWritePermission,
|
|
width, height, colspan, master, masterValue, focus,
|
|
historized, sync)
|
|
self.validable = self.link
|
|
|
|
def getDefaultLayouts(self): return {'view': Table('l-f'), 'edit': 'lrv-f'}
|
|
|
|
def isShowable(self, obj, layoutType):
|
|
res = Type.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.add: return False
|
|
if self.isBack:
|
|
if layoutType == 'edit': return False
|
|
else:
|
|
return obj.getBRefs(self.relationship)
|
|
return res
|
|
|
|
def getValue(self, obj, type='objects', noListIfSingleObj=False,
|
|
startNumber=None, someObjects=False):
|
|
'''Returns the objects linked to p_obj through Ref field "self".
|
|
- 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.'''
|
|
if self.isBack:
|
|
getRefs = obj.reference_catalog.getBackReferences
|
|
uids = [r.sourceUID for r in getRefs(obj, self.relationship)]
|
|
else:
|
|
uids = obj._appy_getSortedField(self.name)
|
|
batchNeeded = startNumber != None
|
|
exec 'refUids = obj.getRaw%s%s()' % (self.name[0].upper(),
|
|
self.name[1:])
|
|
# There may be too much UIDs in sortedField because these fields
|
|
# are not updated when objects are deleted. So we do it now.
|
|
# TODO: do such cleaning on object deletion ?
|
|
toDelete = []
|
|
for uid in uids:
|
|
if uid not in refUids:
|
|
toDelete.append(uid)
|
|
for uid in toDelete:
|
|
uids.remove(uid)
|
|
if not uids:
|
|
# Maybe is there a default value?
|
|
defValue = Type.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 sequenceTypes:
|
|
uids = [o.o.UID() for o in defValue]
|
|
else:
|
|
uids = [defValue.o.UID()]
|
|
# Prepare the result: an instance of SomeObjects, that, in this case,
|
|
# represent a subset of all referred objects
|
|
res = SomeObjects()
|
|
res.totalNumber = res.batchSize = len(uids)
|
|
batchNeeded = startNumber != None
|
|
if batchNeeded:
|
|
res.batchSize = self.maxPerPage
|
|
if startNumber != None:
|
|
res.startNumber = startNumber
|
|
# Get the needed referred objects
|
|
i = res.startNumber
|
|
# Is it possible and more efficient to perform a single query in
|
|
# uid_catalog and get the result in the order of specified uids?
|
|
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.uid_catalog(UID=uids[i])[0].getObject()
|
|
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):
|
|
return value
|
|
|
|
def getRequestValue(self, request):
|
|
return request.get('appy_ref_%s' % self.name, None)
|
|
|
|
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 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 the way p_value is expressed
|
|
refs = value
|
|
if not refs: refs = []
|
|
if type(refs) not in sequenceTypes: refs = [refs]
|
|
for i in range(len(refs)):
|
|
if isinstance(refs[i], basestring):
|
|
# Get the Zope object from the UID
|
|
refs[i] = obj.uid_catalog(UID=refs[i])[0].getObject()
|
|
else:
|
|
refs[i] = refs[i].o # Now we are sure to have a Zope object.
|
|
# Update the field storing on p_obj the ordered list of UIDs
|
|
sortedRefs = obj._appy_getSortedField(self.name)
|
|
del sortedRefs[:]
|
|
for ref in refs: sortedRefs.append(ref.UID())
|
|
# Update the Archetypes Ref field.
|
|
exec 'obj.set%s%s(refs)' % (self.name[0].upper(), self.name[1:])
|
|
|
|
def clone(self, forTool=True):
|
|
'''Produces a clone of myself.'''
|
|
res = Type.clone(self, forTool)
|
|
res.back = copy.copy(self.back)
|
|
if not forTool: return res
|
|
res.link = True
|
|
res.add = False
|
|
res.back.attribute += 'DefaultValue'
|
|
res.back.show = False
|
|
res.select = None # Not callable from tool.
|
|
return res
|
|
|
|
class Computed(Type):
|
|
def __init__(self, validator=None, multiplicity=(0,1), index=None,
|
|
default=None, optional=False, editDefault=False, show='view',
|
|
page='main', group=None, layouts=None, move=0, indexed=False,
|
|
searchable=False, specificReadPermission=False,
|
|
specificWritePermission=False, width=None, height=None,
|
|
colspan=1, method=None, plainText=True, master=None,
|
|
masterValue=None, focus=False, historized=False, sync=True,
|
|
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
|
|
Type.__init__(self, None, multiplicity, index, default, optional,
|
|
False, show, page, group, layouts, move, indexed, False,
|
|
specificReadPermission, specificWritePermission, width,
|
|
height, colspan, master, masterValue, focus, historized,
|
|
sync)
|
|
self.validable = False
|
|
|
|
def callMacro(self, obj, macroPath):
|
|
'''Returns the macro corresponding to p_macroPath. The base folder
|
|
where we search is "skyn".'''
|
|
# Get the special page in Appy that allows to call a macro
|
|
macroPage = obj.skyn.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.skyn
|
|
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, raiseOnError=False)
|
|
|
|
def getFormattedValue(self, obj, value):
|
|
if not isinstance(value, basestring): return str(value)
|
|
return value
|
|
|
|
class Action(Type):
|
|
'''An action is a workflow-independent Python method that can be triggered
|
|
by the user on a given gen-class. For example, the custom installation
|
|
procedure of a gen-application is implemented by an action on the custom
|
|
tool class. An action is rendered as a button.'''
|
|
def __init__(self, validator=None, multiplicity=(1,1), index=None,
|
|
default=None, optional=False, editDefault=False, show=True,
|
|
page='main', group=None, layouts=None, move=0, indexed=False,
|
|
searchable=False, specificReadPermission=False,
|
|
specificWritePermission=False, width=None, height=None,
|
|
colspan=1, action=None, result='computation', confirm=False,
|
|
master=None, masterValue=None, focus=False, historized=False):
|
|
# 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
|
|
Type.__init__(self, None, (0,1), index, default, optional,
|
|
False, show, page, group, layouts, move, indexed, False,
|
|
specificReadPermission, specificWritePermission, width,
|
|
height, colspan, master, masterValue, focus, historized,
|
|
False)
|
|
self.validable = False
|
|
|
|
def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'}
|
|
def __call__(self, obj):
|
|
'''Calls the action on p_obj.'''
|
|
try:
|
|
if type(self.action) in sequenceTypes:
|
|
# There are multiple Python methods
|
|
res = [True, '']
|
|
for act in self.action:
|
|
actRes = act(obj)
|
|
if type(actRes) in 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 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
|
|
except Exception, e:
|
|
res = (False, 'An error occurred. %s' % str(e))
|
|
obj.log(Traceback.get(), type='error')
|
|
return res
|
|
|
|
def isShowable(self, obj, layoutType):
|
|
if layoutType == 'edit': return False
|
|
else: return Type.isShowable(self, obj, layoutType)
|
|
|
|
class Info(Type):
|
|
'''An info is a field whose purpose is to present information
|
|
(text, html...) to the user.'''
|
|
def __init__(self, validator=None, multiplicity=(1,1), index=None,
|
|
default=None, optional=False, editDefault=False, show='view',
|
|
page='main', group=None, layouts=None, move=0, indexed=False,
|
|
searchable=False, specificReadPermission=False,
|
|
specificWritePermission=False, width=None, height=None,
|
|
colspan=1, master=None, masterValue=None, focus=False,
|
|
historized=False):
|
|
Type.__init__(self, None, (0,1), index, default, optional,
|
|
False, show, page, group, layouts, move, indexed, False,
|
|
specificReadPermission, specificWritePermission, width,
|
|
height, colspan, master, masterValue, focus, historized,
|
|
False)
|
|
self.validable = False
|
|
|
|
class Pod(Type):
|
|
'''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.'''
|
|
def __init__(self, validator=None, index=None, default=None,
|
|
optional=False, editDefault=False, show='view',
|
|
page='main', group=None, layouts=None, move=0, indexed=False,
|
|
searchable=False, specificReadPermission=False,
|
|
specificWritePermission=False, width=None, height=None,
|
|
colspan=1, master=None, masterValue=None, focus=False,
|
|
historized=False, template=None, context=None, action=None,
|
|
askAction=False, stylesMapping=None):
|
|
# 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
|
|
Type.__init__(self, None, (0,1), index, default, optional,
|
|
False, show, page, group, layouts, move, indexed,
|
|
searchable, specificReadPermission,
|
|
specificWritePermission, width, height, colspan, master,
|
|
masterValue, focus, historized, False)
|
|
self.validable = False
|
|
|
|
# Workflow-specific types ------------------------------------------------------
|
|
class Role:
|
|
'''Represents a role.'''
|
|
ploneRoles = ('Manager', 'Member', 'Owner', 'Reviewer', 'Anonymous',
|
|
'Authenticated')
|
|
ploneLocalRoles = ('Owner',)
|
|
ploneUngrantableRoles = ('Anonymous', 'Authenticated')
|
|
def __init__(self, name, local=False, grantable=True):
|
|
self.name = name
|
|
self.local = local # True if it can be used as local role only.
|
|
# It is a standard Plone role or an application-specific one?
|
|
self.plone = name in self.ploneRoles
|
|
if self.plone and (name in self.ploneLocalRoles):
|
|
self.local = True
|
|
self.grantable = grantable
|
|
if self.plone and (name in self.ploneUngrantableRoles):
|
|
self.grantable = False
|
|
# An ungrantable role is one that is, like the Anonymous or
|
|
# Authenticated roles, automatically attributed to a user.
|
|
|
|
class State:
|
|
def __init__(self, permissions, initial=False, phase='main', show=True):
|
|
self.usedRoles = {}
|
|
# The following dict ~{s_permissionName:[s_roleName|Role_role]}~
|
|
# gives, for every permission managed by a workflow, the list of roles
|
|
# for which the permission is granted in this state. Standard
|
|
# permissions are 'read', 'write' and 'delete'.
|
|
self.permissions = permissions
|
|
self.initial = initial
|
|
self.phase = phase
|
|
self.show = show
|
|
# Standardize the way roles are expressed within self.permissions
|
|
self.standardizeRoles()
|
|
|
|
def getRole(self, role):
|
|
'''p_role can be the name of a role or a Role instance. If it is the
|
|
name of a role, this method returns self.usedRoles[role] if it
|
|
exists, or creates a Role instance, puts it in self.usedRoles and
|
|
returns it else. If it is a Role instance, the method stores it in
|
|
self.usedRoles if it not in it yet and returns it.'''
|
|
if isinstance(role, basestring):
|
|
if role in self.usedRoles:
|
|
return self.usedRoles[role]
|
|
else:
|
|
theRole = Role(role)
|
|
self.usedRoles[role] = theRole
|
|
return theRole
|
|
else:
|
|
if role.name not in self.usedRoles:
|
|
self.usedRoles[role.name] = role
|
|
return role
|
|
|
|
def standardizeRoles(self):
|
|
'''This method converts, within self.permissions, every role to a
|
|
Role instance. Every used role is stored in self.usedRoles.'''
|
|
for permission, roles in self.permissions.items():
|
|
if isinstance(roles, basestring) or isinstance(roles, Role):
|
|
self.permissions[permission] = [self.getRole(roles)]
|
|
elif roles:
|
|
rolesList = []
|
|
for role in roles:
|
|
rolesList.append(self.getRole(role))
|
|
self.permissions[permission] = rolesList
|
|
|
|
def getUsedRoles(self): return self.usedRoles.values()
|
|
|
|
def getTransitions(self, transitions, selfIsFromState=True):
|
|
'''Among p_transitions, returns those whose fromState is p_self (if
|
|
p_selfIsFromState is True) or those whose toState is p_self (if
|
|
p_selfIsFromState is False).'''
|
|
res = []
|
|
for t in transitions:
|
|
if self in t.getStates(selfIsFromState):
|
|
res.append(t)
|
|
return res
|
|
|
|
def getPermissions(self):
|
|
'''If you get the permissions mapping through self.permissions, dict
|
|
values may be of different types (a list of roles, a single role or
|
|
None). Iy you call this method, you will always get a list which
|
|
may be empty.'''
|
|
res = {}
|
|
for permission, roleValue in self.permissions.iteritems():
|
|
if roleValue == None:
|
|
res[permission] = []
|
|
elif isinstance(roleValue, basestring):
|
|
res[permission] = [roleValue]
|
|
else:
|
|
res[permission] = roleValue
|
|
return res
|
|
|
|
class Transition:
|
|
def __init__(self, states, condition=True, action=None, notify=None,
|
|
show=True, confirm=False):
|
|
self.states = states # In its simpler form, it is a tuple with 2
|
|
# states: (fromState, toState). But it can also be a tuple of several
|
|
# (fromState, toState) sub-tuples. This way, you may define only 1
|
|
# transition at several places in the state-transition diagram. It may
|
|
# be useful for "undo" transitions, for example.
|
|
self.condition = condition
|
|
if isinstance(condition, basestring):
|
|
# The condition specifies the name of a role.
|
|
self.condition = Role(condition)
|
|
self.action = action
|
|
self.notify = notify # If not None, it is a method telling who must be
|
|
# notified by email after the transition has been executed.
|
|
self.show = show # If False, the end user will not be able to trigger
|
|
# the transition. It will only be possible by code.
|
|
self.confirm = confirm # If True, a confirm popup will show up.
|
|
|
|
def getUsedRoles(self):
|
|
'''self.condition can specify a role.'''
|
|
res = []
|
|
if isinstance(self.condition, Role):
|
|
res.append(self.condition)
|
|
return res
|
|
|
|
def isSingle(self):
|
|
'''If this transition is only defined between 2 states, returns True.
|
|
Else, returns False.'''
|
|
return isinstance(self.states[0], State)
|
|
|
|
def isShowable(self, workflow, obj):
|
|
'''Is this transition showable?'''
|
|
if callable(self.show):
|
|
return self.show(workflow, obj.appy())
|
|
else:
|
|
return self.show
|
|
|
|
def getStates(self, fromStates=True):
|
|
'''Returns the fromState(s) if p_fromStates is True, the toState(s)
|
|
else. If you want to get the states grouped in tuples
|
|
(fromState, toState), simply use self.states.'''
|
|
res = []
|
|
stateIndex = 1
|
|
if fromStates:
|
|
stateIndex = 0
|
|
if self.isSingle():
|
|
res.append(self.states[stateIndex])
|
|
else:
|
|
for states in self.states:
|
|
theState = states[stateIndex]
|
|
if theState not in res:
|
|
res.append(theState)
|
|
return res
|
|
|
|
def hasState(self, state, isFrom):
|
|
'''If p_isFrom is True, this method returns True if p_state is a
|
|
starting state for p_self. If p_isFrom is False, this method returns
|
|
True if p_state is an ending state for p_self.'''
|
|
stateIndex = 1
|
|
if isFrom:
|
|
stateIndex = 0
|
|
if self.isSingle():
|
|
res = state == self.states[stateIndex]
|
|
else:
|
|
res = False
|
|
for states in self.states:
|
|
if states[stateIndex] == state:
|
|
res = True
|
|
break
|
|
return res
|
|
|
|
class Permission:
|
|
'''If you need to define a specific read or write permission of a given
|
|
attribute of an Appy type, you use the specific boolean parameters
|
|
"specificReadPermission" or "specificWritePermission" for this attribute.
|
|
When you want to refer to those specific read or write permissions when
|
|
defining a workflow, for example, you need to use instances of
|
|
"ReadPermission" and "WritePermission", the 2 children classes of this
|
|
class. For example, if you need to refer to write permission of
|
|
attribute "t1" of class A, write: WritePermission("A.t1") or
|
|
WritePermission("x.y.A.t1") if class A is not in the same module as
|
|
where you instantiate the class.
|
|
|
|
Note that this holds only if you use attributes "specificReadPermission"
|
|
and "specificWritePermission" as booleans. When defining named
|
|
(string) permissions, for referring to it you simply use those strings,
|
|
you do not create instances of ReadPermission or WritePermission.'''
|
|
def __init__(self, fieldDescriptor):
|
|
self.fieldDescriptor = fieldDescriptor
|
|
|
|
class ReadPermission(Permission): pass
|
|
class WritePermission(Permission): pass
|
|
|
|
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
|
|
|
|
# ------------------------------------------------------------------------------
|
|
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 Model: pass
|
|
class Tool(Model):
|
|
'''If you want so define a custom tool class, she must inherit from this
|
|
class.'''
|
|
class User(Model):
|
|
'''If you want to extend or modify the User class, subclass me.'''
|
|
|
|
# ------------------------------------------------------------------------------
|
|
class Config:
|
|
'''If you want to specify some configuration parameters for appy.gen and
|
|
your application, please create an instance of this class and modify its
|
|
attributes. You may put your instance anywhere in your application
|
|
(main package, sub-package, etc).'''
|
|
|
|
# The default Config instance, used if the application does not give one.
|
|
defaultConfig = None
|
|
def getDefault():
|
|
if not Config.defaultConfig:
|
|
Config.defaultConfig = Config()
|
|
return Config.defaultConfig
|
|
getDefault = staticmethod(getDefault)
|
|
|
|
def __init__(self):
|
|
# For every language code that you specify in this list, appy.gen will
|
|
# produce and maintain translation files.
|
|
self.languages = ['en']
|
|
# If languageSelector is True, on every page, a language selector will
|
|
# allow to switch between languages defined in self.languages. Else,
|
|
# the browser-defined language will be used for choosing the language
|
|
# of returned pages.
|
|
self.languageSelector = False
|
|
# People having one of these roles will be able to create instances
|
|
# of classes defined in your application.
|
|
self.defaultCreators = ['Manager', 'Owner']
|
|
# If True, the following flag will produce a minimalist Plone, where
|
|
# some actions, portlets or other stuff less relevant for building
|
|
# web applications, are removed or hidden. Using this produces
|
|
# effects on your whole Plone site!
|
|
self.minimalistPlone = False
|
|
# If you want to replace the Plone front page with a page coming from
|
|
# your application, use the following parameter. Setting
|
|
# frontPage = True will replace the Plone front page with a page
|
|
# whose content will come fron i18n label "front_page_text".
|
|
self.frontPage = False
|
|
# If you don't need the portlet that appy.gen has generated for your
|
|
# application, set the following parameter to False.
|
|
self.showPortlet = True
|
|
# Number of translations for every page on a Translation object
|
|
self.translationsPerPage = 30
|
|
# Language that will be used as a basis for translating to other
|
|
# languages.
|
|
self.sourceLanguage = 'en'
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Special field "type" is mandatory for every class. If one class does not
|
|
# define it, we will add a copy of the instance defined below.
|
|
title = String(multiplicity=(1,1), show='edit')
|
|
title.init('title', None, 'appy')
|
|
state = String()
|
|
state.init('state', None, 'appy')
|
|
# ------------------------------------------------------------------------------
|