2010-11-23 10:25:00 -06:00
|
|
|
# -*- coding: utf-8 -*-
|
2009-06-29 07:06:01 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
2012-02-16 11:13:51 -06:00
|
|
|
import re, time, copy, sys, types, os, os.path, mimetypes, string, StringIO, \
|
|
|
|
random
|
2011-10-26 03:21:09 -05:00
|
|
|
from appy import Object
|
2010-08-05 11:23:17 -05:00
|
|
|
from appy.gen.layout import Table
|
|
|
|
from appy.gen.layout import defaultFieldLayouts
|
2012-05-03 03:51:54 -05:00
|
|
|
from appy.gen.mail import sendNotification
|
2012-10-05 09:38:15 -05:00
|
|
|
from appy.gen.indexer import defaultIndexes, XhtmlTextExtractor
|
2012-01-12 14:49:23 -06:00
|
|
|
from appy.gen.utils import GroupDescr, Keywords, getClassName, SomeObjects
|
2011-02-16 06:43:58 -06:00
|
|
|
import appy.pod
|
|
|
|
from appy.pod.renderer import Renderer
|
2011-04-26 13:49:33 -05:00
|
|
|
from appy.shared.data import countries
|
2012-05-14 10:35:34 -05:00
|
|
|
from appy.shared.xml_parser import XhtmlCleaner
|
2013-01-08 09:58:29 -06:00
|
|
|
from appy.shared.diff import HtmlDiff
|
2011-12-15 15:56:53 -06:00
|
|
|
from appy.shared.utils import Traceback, getOsTempFolder, formatNumber, \
|
2012-05-14 10:35:34 -05:00
|
|
|
FileWrapper, sequenceTypes
|
2009-06-29 07:06:01 -05:00
|
|
|
|
|
|
|
# Default Appy permissions -----------------------------------------------------
|
|
|
|
r, w, d = ('read', 'write', 'delete')
|
2010-08-05 11:23:17 -05:00
|
|
|
digit = re.compile('[0-9]')
|
|
|
|
alpha = re.compile('[a-zA-Z0-9]')
|
2009-11-17 08:46:41 -06:00
|
|
|
letter = re.compile('[a-zA-Z]')
|
2011-10-26 03:21:09 -05:00
|
|
|
nullValues = (None, '', [])
|
2010-09-02 09:16:08 -05:00
|
|
|
validatorTypes = (types.FunctionType, types.UnboundMethodType,
|
|
|
|
type(re.compile('')))
|
2010-08-05 11:23:17 -05:00
|
|
|
emptyTuple = ()
|
2011-03-04 03:30:45 -06:00
|
|
|
labelTypes = ('label', 'descr', 'help')
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2011-10-01 15:40:13 -05:00
|
|
|
def initMasterValue(v):
|
2011-10-04 13:12:58 -05:00
|
|
|
'''Standardizes p_v as a list of strings.'''
|
2011-11-10 14:59:02 -06:00
|
|
|
if not isinstance(v, bool) and not v: res = []
|
2011-10-01 15:40:13 -05:00
|
|
|
elif type(v) not in sequenceTypes: res = [v]
|
|
|
|
else: res = v
|
2011-10-04 13:12:58 -05:00
|
|
|
return [str(v) for v in res]
|
2011-10-01 15:40:13 -05:00
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
# Descriptor classes used for refining descriptions of elements in types
|
|
|
|
# (pages, groups,...) ----------------------------------------------------------
|
|
|
|
class Page:
|
2009-10-20 09:57:00 -05:00
|
|
|
'''Used for describing a page, its related phase, show condition, etc.'''
|
2010-10-19 03:47:42 -05:00
|
|
|
subElements = ('save', 'cancel', 'previous', 'next')
|
|
|
|
def __init__(self, name, phase='main', show=True, showSave=True,
|
|
|
|
showCancel=True, showPrevious=True, showNext=True):
|
2009-06-29 07:06:01 -05:00
|
|
|
self.name = name
|
|
|
|
self.phase = phase
|
|
|
|
self.show = show
|
2010-10-19 03:47:42 -05:00
|
|
|
# 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)
|
2011-01-14 02:06:25 -06:00
|
|
|
if callable(show):
|
|
|
|
show = show(obj.appy())
|
2010-10-19 03:47:42 -05:00
|
|
|
# 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
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
class Group:
|
|
|
|
'''Used for describing a group of widgets within a page.'''
|
2010-10-14 07:43:56 -05:00
|
|
|
def __init__(self, name, columns=['100%'], wide=True, style='section2',
|
2010-08-05 11:23:17 -05:00
|
|
|
hasLabel=True, hasDescr=False, hasHelp=False,
|
2010-09-13 14:04:10 -05:00
|
|
|
hasHeaders=False, group=None, colspan=1, align='center',
|
2010-12-06 04:11:40 -06:00
|
|
|
valign='top', css_class='', master=None, masterValue=None,
|
2012-11-14 10:40:52 -06:00
|
|
|
cellpadding=1, cellspacing=1, cellgap='0.6em', label=None,
|
|
|
|
translated=None):
|
2010-08-05 11:23:17 -05:00
|
|
|
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
|
2011-09-10 18:59:22 -05:00
|
|
|
# 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 = ''
|
2010-08-05 11:23:17 -05:00
|
|
|
# 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
|
2010-09-13 14:04:10 -05:00
|
|
|
self.align = align
|
2010-08-27 01:59:53 -05:00
|
|
|
self.valign = valign
|
2010-12-06 04:11:40 -06:00
|
|
|
self.cellpadding = cellpadding
|
|
|
|
self.cellspacing = cellspacing
|
2011-03-24 10:21:57 -05:00
|
|
|
# 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
|
2010-08-05 11:23:17 -05:00
|
|
|
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
|
2010-10-14 07:43:56 -05:00
|
|
|
self.css_class = css_class
|
|
|
|
self.master = master
|
2011-10-01 15:40:13 -05:00
|
|
|
self.masterValue = initMasterValue(masterValue)
|
|
|
|
if master: master.slaves.append(self)
|
|
|
|
self.label = label # See similar attr of Type class.
|
2012-11-14 10:40:52 -06:00
|
|
|
# If a translated name is already given here, we will use it instead of
|
|
|
|
# trying to translate the group label.
|
|
|
|
self.translated = translated
|
2010-08-05 11:23:17 -05:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2010-11-05 07:05:10 -05:00
|
|
|
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()
|
|
|
|
|
2012-11-06 04:32:39 -06:00
|
|
|
def generateLabels(self, messages, classDescr, walkedGroups,
|
|
|
|
forSearch=False):
|
2010-08-05 11:23:17 -05:00
|
|
|
'''This method allows to generate all the needed i18n labels related to
|
2012-11-06 04:32:39 -06:00
|
|
|
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'
|
2010-08-05 11:23:17 -05:00
|
|
|
if self.hasLabel:
|
2012-11-06 04:32:39 -06:00
|
|
|
msgId = '%s_%s_%s' % (classDescr.name, gp, self.name)
|
|
|
|
messages.append(msgId, self.name)
|
2010-08-05 11:23:17 -05:00
|
|
|
if self.hasDescr:
|
2012-11-06 04:32:39 -06:00
|
|
|
msgId = '%s_%s_%s_descr' % (classDescr.name, gp, self.name)
|
|
|
|
messages.append(msgId, ' ', nice=False)
|
2010-08-05 11:23:17 -05:00
|
|
|
if self.hasHelp:
|
2012-11-06 04:32:39 -06:00
|
|
|
msgId = '%s_%s_%s_help' % (classDescr.name, gp, self.name)
|
|
|
|
messages.append(msgId, ' ', nice=False)
|
2010-08-05 11:23:17 -05:00
|
|
|
if self.hasHeaders:
|
|
|
|
for i in range(self.nbOfHeaders):
|
2012-11-06 04:32:39 -06:00
|
|
|
msgId = '%s_%s_%s_col%d' % (classDescr.name, gp, self.name, i+1)
|
|
|
|
messages.append(msgId, ' ', nice=False)
|
2010-08-05 11:23:17 -05:00
|
|
|
walkedGroups.add(self)
|
2011-09-10 18:59:22 -05:00
|
|
|
if self.group and (self.group not in walkedGroups) and \
|
|
|
|
not self.group.label:
|
2010-08-05 11:23:17 -05:00
|
|
|
# We remember walked groups for avoiding infinite recursion.
|
2012-11-14 04:36:48 -06:00
|
|
|
self.group.generateLabels(messages, classDescr, walkedGroups,
|
|
|
|
forSearch=forSearch)
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2012-11-14 04:36:48 -06:00
|
|
|
def insertInto(self, widgets, groupDescrs, page, metaType, forSearch=False):
|
2010-08-05 11:23:17 -05:00
|
|
|
'''Inserts the GroupDescr instance corresponding to this Group instance
|
|
|
|
into p_widgets, the recursive structure used for displaying all
|
2012-11-14 04:36:48 -06:00
|
|
|
widgets in a given p_page (or all searches), and returns this
|
|
|
|
GroupDescr instance.'''
|
2010-08-05 11:23:17 -05:00
|
|
|
# First, create the corresponding GroupDescr if not already in
|
|
|
|
# p_groupDescrs.
|
|
|
|
if self.name not in groupDescrs:
|
2012-11-14 04:36:48 -06:00
|
|
|
groupDescr = groupDescrs[self.name] = \
|
|
|
|
GroupDescr(self, page, metaType, forSearch=forSearch).get()
|
2010-08-05 11:23:17 -05:00
|
|
|
# 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,
|
2012-11-14 04:36:48 -06:00
|
|
|
page, metaType, forSearch=forSearch)
|
2010-08-05 11:23:17 -05:00
|
|
|
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
|
|
|
|
|
2009-10-20 09:57:00 -05:00
|
|
|
class Import:
|
|
|
|
'''Used for describing the place where to find the data to use for creating
|
|
|
|
an object.'''
|
2010-01-06 11:36:16 -06:00
|
|
|
def __init__(self, path, onElement=None, headers=(), sort=None):
|
2009-10-20 09:57:00 -05:00
|
|
|
self.id = 'import'
|
|
|
|
self.path = path
|
2010-01-06 11:36:16 -06:00
|
|
|
# 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
|
2009-10-20 09:57:00 -05:00
|
|
|
|
2009-10-30 15:31:39 -05:00
|
|
|
class Search:
|
|
|
|
'''Used for specifying a search for a given type.'''
|
2012-05-25 07:27:53 -05:00
|
|
|
def __init__(self, name, group=None, sortBy='', sortOrder='asc', limit=None,
|
2012-11-14 10:40:52 -06:00
|
|
|
default=False, colspan=1, translated=None,
|
|
|
|
translatedDescr=None, **fields):
|
2009-10-30 15:31:39 -05:00
|
|
|
self.name = name
|
2012-11-06 04:32:39 -06:00
|
|
|
# Searches may be visually grouped in the portlet.
|
|
|
|
self.group = Group.get(group)
|
2009-10-30 15:31:39 -05:00
|
|
|
self.sortBy = sortBy
|
2012-05-25 07:27:53 -05:00
|
|
|
self.sortOrder = sortOrder
|
2009-10-30 15:31:39 -05:00
|
|
|
self.limit = limit
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
# If this search is the default one, it will be triggered by clicking
|
|
|
|
# on main link.
|
|
|
|
self.default = default
|
2012-11-14 04:36:48 -06:00
|
|
|
self.colspan = colspan
|
2012-11-14 10:40:52 -06:00
|
|
|
# If a translated name or description is already given here, we will
|
|
|
|
# use it instead of trying to translate from labels.
|
|
|
|
self.translated = translated
|
|
|
|
self.translatedDescr = translatedDescr
|
2010-12-06 04:11:40 -06:00
|
|
|
# In the dict below, keys are indexed field names and values are
|
|
|
|
# search values.
|
|
|
|
self.fields = fields
|
2013-02-18 08:03:26 -06:00
|
|
|
|
2010-04-30 05:05:29 -05:00
|
|
|
@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'
|
2011-11-25 11:01:20 -06:00
|
|
|
else: return 'SortableTitle'
|
|
|
|
# Indeed, for field 'title', Appy has a specific index
|
2012-09-26 16:13:02 -05:00
|
|
|
# 'SortableTitle', because index 'Title' is a TextIndex
|
2010-04-30 05:05:29 -05:00
|
|
|
# (for searchability) and can't be used for sorting.
|
2012-04-25 09:21:23 -05:00
|
|
|
elif fieldName == 'state': return 'State'
|
2012-09-19 04:29:29 -05:00
|
|
|
elif fieldName in defaultIndexes: return fieldName
|
2010-04-30 05:05:29 -05:00
|
|
|
else:
|
|
|
|
return 'get%s%s'% (fieldName[0].upper(),fieldName[1:])
|
2013-02-18 08:03:26 -06:00
|
|
|
|
2010-04-30 05:05:29 -05:00
|
|
|
@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.'''
|
2013-02-18 08:03:26 -06:00
|
|
|
if fieldName in ('title', 'SearchableText'):
|
|
|
|
# Title and SearchableText are TextIndex indexes. We must split
|
|
|
|
# p_fieldValue into keywords.
|
2012-09-26 16:13:02 -05:00
|
|
|
res = Keywords(fieldValue).get()
|
2010-04-30 05:05:29 -05:00
|
|
|
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
|
2009-10-30 15:31:39 -05:00
|
|
|
|
2013-02-18 08:03:26 -06:00
|
|
|
def updateSearchCriteria(self, criteria, advanced=False):
|
|
|
|
'''This method updates dict p_criteria with all the search criteria
|
|
|
|
corresponding to this Search instance. If p_advanced is True,
|
|
|
|
p_criteria correspond to an advanced search, to be stored in the
|
|
|
|
session: in this case we need to keep the Appy names for parameters
|
|
|
|
sortBy and sortOrder (and not "resolve" them to Zope's sort_on and
|
|
|
|
sort_order).'''
|
|
|
|
# Put search criteria in p_criteria
|
|
|
|
for fieldName, fieldValue in self.fields.iteritems():
|
|
|
|
# Management of searches restricted to objects linked through a
|
|
|
|
# Ref field: not implemented yet.
|
|
|
|
if fieldName == '_ref': continue
|
|
|
|
# Make the correspondence between the name of the field and the
|
|
|
|
# name of the corresponding index, excepted if advanced is True: in
|
|
|
|
# that case, the correspondence will be done later.
|
|
|
|
if not advanced:
|
|
|
|
attrName = Search.getIndexName(fieldName)
|
|
|
|
# Express the field value in the way needed by the index
|
|
|
|
criteria[attrName]= Search.getSearchValue(fieldName, fieldValue)
|
|
|
|
else:
|
|
|
|
criteria[fieldName]= fieldValue
|
|
|
|
# Add a sort order if specified
|
|
|
|
if self.sortBy:
|
|
|
|
if not advanced:
|
|
|
|
criteria['sort_on'] = Search.getIndexName(self.sortBy,
|
|
|
|
usage='sort')
|
|
|
|
if self.sortOrder == 'desc': criteria['sort_order'] = 'reverse'
|
|
|
|
else: criteria['sort_order'] = None
|
|
|
|
else:
|
|
|
|
criteria['sortBy'] = self.sortBy
|
|
|
|
criteria['sortOrder'] = self.sortOrder
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
class Type:
|
|
|
|
'''Basic abstract class for defining any appy type.'''
|
2012-05-05 10:04:19 -05:00
|
|
|
# Those attributes can be overridden by subclasses for defining,
|
|
|
|
# respectively, names of CSS and Javascript files that are required by this
|
|
|
|
# field, keyed by layoutType.
|
|
|
|
cssFiles = {}
|
|
|
|
jsFiles = {}
|
2012-09-24 09:32:16 -05:00
|
|
|
dLayouts = 'lrv-d-f'
|
2012-05-05 10:04:19 -05:00
|
|
|
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
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,
|
2013-02-19 02:57:02 -06:00
|
|
|
sdefault, scolspan, swidth, sheight):
|
2009-06-29 07:06:01 -05:00
|
|
|
# The validator restricts which values may be defined. It can be an
|
|
|
|
# interval (1,None), a list of string values ['choice1', 'choice2'],
|
|
|
|
# a regular expression, a custom function, a Selection instance, etc.
|
|
|
|
self.validator = validator
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
# Multiplicity is a 2-tuple indicating the minimum and maximum
|
2009-06-29 07:06:01 -05:00
|
|
|
# occurrences of values.
|
|
|
|
self.multiplicity = multiplicity
|
2010-08-05 11:23:17 -05:00
|
|
|
# Is the field required or not ? (derived from multiplicity)
|
|
|
|
self.required = self.multiplicity[0] > 0
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
# Default value
|
|
|
|
self.default = default
|
2009-06-29 07:06:01 -05:00
|
|
|
# Must the field be visible or not?
|
|
|
|
self.show = show
|
|
|
|
# When displaying/editing the whole object, on what page and phase must
|
2010-10-19 03:47:42 -05:00
|
|
|
# this field value appear?
|
|
|
|
self.page = Page.get(page)
|
|
|
|
self.pageName = self.page.name
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
# Within self.page, in what group of fields must this one appear?
|
2010-08-05 11:23:17 -05:00
|
|
|
self.group = Group.get(group)
|
2009-06-29 07:06:01 -05:00
|
|
|
# The following attribute allows to move a field back to a previous
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
# position (useful for moving fields above predefined ones).
|
2009-06-29 07:06:01 -05:00
|
|
|
self.move = move
|
2009-10-30 15:31:39 -05:00
|
|
|
# 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.
|
2009-06-29 07:06:01 -05:00
|
|
|
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
|
2010-08-12 04:56:42 -05:00
|
|
|
# edit instances of the whole type. If you want a given attribute
|
2009-06-29 07:06:01 -05:00
|
|
|
# to be protected by specific permissions, set one or the 2 next boolean
|
2010-08-12 04:56:42 -05:00
|
|
|
# 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
|
2011-01-14 02:06:25 -06:00
|
|
|
# of the form: "<yourAppName>: Write|Read ---". If, for example, I want
|
2010-08-12 04:56:42 -05:00
|
|
|
# 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.
|
2009-06-29 07:06:01 -05:00
|
|
|
self.specificReadPermission = specificReadPermission
|
|
|
|
self.specificWritePermission = specificWritePermission
|
|
|
|
# Widget width and height
|
|
|
|
self.width = width
|
|
|
|
self.height = height
|
2011-05-05 09:44:06 -05:00
|
|
|
# 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
|
2010-08-05 11:23:17 -05:00
|
|
|
# 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
|
2011-10-01 15:40:13 -05:00
|
|
|
# The list of slaves of this field, if it is a master
|
|
|
|
self.slaves = []
|
2009-06-29 07:06:01 -05:00
|
|
|
# The behaviour of this field may depend on another, "master" field
|
|
|
|
self.master = master
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
if master: self.master.slaves.append(self)
|
2009-06-29 07:06:01 -05:00
|
|
|
# When master has some value(s), there is impact on this field.
|
2011-10-01 15:40:13 -05:00
|
|
|
self.masterValue = initMasterValue(masterValue)
|
2009-10-25 15:42:08 -05:00
|
|
|
# If a field must retain attention in a particular way, set focus=True.
|
|
|
|
# It will be rendered in a special way.
|
|
|
|
self.focus = focus
|
2009-12-14 13:22:55 -06:00
|
|
|
# If we must keep track of changes performed on a field, "historized"
|
|
|
|
# must be set to True.
|
|
|
|
self.historized = historized
|
2010-09-17 02:27:14 -05:00
|
|
|
# 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)
|
2011-03-04 03:30:45 -06:00
|
|
|
# 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)
|
2009-06-29 07:06:01 -05:00
|
|
|
self.id = id(self)
|
|
|
|
self.type = self.__class__.__name__
|
|
|
|
self.pythonType = None # The True corresponding Python type
|
2010-08-05 11:23:17 -05:00
|
|
|
# Get the layouts. Consult layout.py for more info about layouts.
|
2010-09-13 14:04:10 -05:00
|
|
|
self.layouts = self.formatLayouts(layouts)
|
2010-08-05 11:23:17 -05:00
|
|
|
# Can we filter this field?
|
|
|
|
self.filterable = False
|
|
|
|
# Can this field have values that can be edited and validated?
|
|
|
|
self.validable = True
|
2011-09-06 14:46:57 -05:00
|
|
|
# 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
|
2012-12-14 02:23:33 -06:00
|
|
|
# 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
|
2013-02-19 02:57:02 -06:00
|
|
|
# Width and height for the search widget
|
|
|
|
self.swidth = swidth or width
|
|
|
|
self.sheight = sheight or height
|
2010-08-05 11:23:17 -05:00
|
|
|
|
|
|
|
def init(self, name, klass, appName):
|
|
|
|
'''When the application server starts, this secondary constructor is
|
2011-09-06 14:46:57 -05:00
|
|
|
called for storing the name of the Appy field (p_name) and other
|
2010-08-05 11:23:17 -05:00
|
|
|
attributes that are based on the name of the Appy p_klass, and the
|
|
|
|
application name (p_appName).'''
|
2011-09-14 14:01:58 -05:00
|
|
|
if hasattr(self, 'name'): return # Already initialized
|
2010-08-05 11:23:17 -05:00
|
|
|
self.name = name
|
2011-09-14 14:01:58 -05:00
|
|
|
# Determine prefix for this class
|
|
|
|
if not klass: prefix = appName
|
|
|
|
else: prefix = getClassName(klass, appName)
|
2011-03-25 12:03:45 -05:00
|
|
|
# Recompute the ID (and derived attributes) that may have changed if
|
|
|
|
# we are in debug mode (because we recreate new Type instances).
|
|
|
|
self.id = id(self)
|
2011-10-01 15:40:13 -05:00
|
|
|
# Remember master name on every slave
|
|
|
|
for slave in self.slaves: slave.masterName = name
|
2010-08-05 11:23:17 -05:00
|
|
|
# Determine ids of i18n labels for this field
|
2011-09-06 14:46:57 -05:00
|
|
|
labelName = name
|
2011-09-14 14:01:58 -05:00
|
|
|
trPrefix = None
|
2011-09-06 14:46:57 -05:00
|
|
|
if self.label:
|
2011-09-14 14:01:58 -05:00
|
|
|
if isinstance(self.label, basestring): trPrefix = self.label
|
|
|
|
else: # It is a tuple (trPrefix, name)
|
2011-09-06 14:46:57 -05:00
|
|
|
if self.label[1]: labelName = self.label[1]
|
2011-09-14 14:01:58 -05:00
|
|
|
if self.label[0]: trPrefix = self.label[0]
|
|
|
|
if not trPrefix:
|
|
|
|
trPrefix = prefix
|
2011-09-06 14:46:57 -05:00
|
|
|
# Determine name to use for i18n
|
2011-09-14 14:01:58 -05:00
|
|
|
self.labelId = '%s_%s' % (trPrefix, labelName)
|
2010-08-05 11:23:17 -05:00
|
|
|
self.descrId = self.labelId + '_descr'
|
|
|
|
self.helpId = self.labelId + '_help'
|
|
|
|
# Determine read and write permissions for this field
|
2010-08-12 04:56:42 -05:00
|
|
|
rp = self.specificReadPermission
|
|
|
|
if rp and not isinstance(rp, basestring):
|
2010-08-05 11:23:17 -05:00
|
|
|
self.readPermission = '%s: Read %s %s' % (appName, prefix, name)
|
2010-08-12 04:56:42 -05:00
|
|
|
elif rp and isinstance(rp, basestring):
|
|
|
|
self.readPermission = rp
|
2010-08-05 11:23:17 -05:00
|
|
|
else:
|
|
|
|
self.readPermission = 'View'
|
2010-08-12 04:56:42 -05:00
|
|
|
wp = self.specificWritePermission
|
|
|
|
if wp and not isinstance(wp, basestring):
|
2010-08-05 11:23:17 -05:00
|
|
|
self.writePermission = '%s: Write %s %s' % (appName, prefix, name)
|
2010-08-12 04:56:42 -05:00
|
|
|
elif wp and isinstance(wp, basestring):
|
|
|
|
self.writePermission = wp
|
2010-08-05 11:23:17 -05:00
|
|
|
else:
|
|
|
|
self.writePermission = 'Modify portal content'
|
|
|
|
if isinstance(self, Ref) and not self.isBack:
|
2011-09-14 14:01:58 -05:00
|
|
|
# We must initialise the corresponding back reference
|
|
|
|
self.back.klass = klass
|
|
|
|
self.back.init(self.back.attribute, self.klass, appName)
|
2011-10-19 02:37:44 -05:00
|
|
|
if isinstance(self, List):
|
|
|
|
for subName, subField in self.fields:
|
|
|
|
fullName = '%s_%s' % (name, subName)
|
|
|
|
subField.init(fullName, klass, appName)
|
|
|
|
subField.name = '%s*%s' % (name, subName)
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2010-11-13 10:54:08 -06:00
|
|
|
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
|
2011-09-14 14:01:58 -05:00
|
|
|
if isinstance(self, Ref) and self.isBack: return self
|
2010-11-13 10:54:08 -06:00
|
|
|
res.init(self.name, klass, obj.getProductConfig().PROJECTNAME)
|
|
|
|
return res
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
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
|
|
|
|
|
2010-09-20 04:33:54 -05:00
|
|
|
def isSortable(self, usage):
|
2010-04-30 07:43:44 -05:00
|
|
|
'''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")?'''
|
2010-09-20 04:33:54 -05:00
|
|
|
if usage == 'search':
|
2011-01-28 07:36:30 -06:00
|
|
|
return self.indexed and not self.isMultiValued() and not \
|
|
|
|
((self.type == 'String') and self.isSelection())
|
2010-04-30 07:43:44 -05:00
|
|
|
elif usage == 'ref':
|
|
|
|
return self.type in ('Integer', 'Float', 'Boolean', 'Date') or \
|
|
|
|
((self.type == 'String') and (self.format == 0))
|
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
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
|
2011-09-08 09:33:16 -05:00
|
|
|
if layoutType == 'edit': perm = self.writePermission
|
|
|
|
else: perm = self.readPermission
|
|
|
|
if not obj.allows(perm): return False
|
2010-08-05 11:23:17 -05:00
|
|
|
# Evaluate self.show
|
|
|
|
if callable(self.show):
|
2011-01-14 02:06:25 -06:00
|
|
|
res = self.callMethod(obj, self.show)
|
2010-08-05 11:23:17 -05:00
|
|
|
else:
|
|
|
|
res = self.show
|
2012-03-06 10:02:41 -06:00
|
|
|
# Take into account possible values 'view', 'edit', 'result'...
|
|
|
|
if type(res) in sequenceTypes:
|
|
|
|
for r in res:
|
|
|
|
if r == layoutType: return True
|
|
|
|
return False
|
|
|
|
elif res in ('view', 'edit', 'result'):
|
|
|
|
return res == layoutType
|
2011-01-28 07:36:30 -06:00
|
|
|
return bool(res)
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2010-11-05 07:05:10 -05:00
|
|
|
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)
|
2011-10-01 15:40:13 -05:00
|
|
|
# reqValue can be a list or not
|
|
|
|
if type(reqValue) not in sequenceTypes:
|
|
|
|
return reqValue in masterValue
|
|
|
|
else:
|
2010-11-05 07:05:10 -05:00
|
|
|
for m in masterValue:
|
|
|
|
for r in reqValue:
|
|
|
|
if m == r: return True
|
|
|
|
|
2010-09-17 02:27:14 -05:00
|
|
|
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
|
|
|
|
|
2011-03-04 03:30:45 -06:00
|
|
|
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}
|
|
|
|
|
2010-09-13 14:04:10 -05:00
|
|
|
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
|
2010-11-22 08:34:04 -06:00
|
|
|
layouts = self.computeDefaultLayouts()
|
2010-09-13 14:04:10 -05:00
|
|
|
else:
|
2010-10-29 07:36:36 -05:00
|
|
|
if isinstance(layouts, basestring):
|
2010-09-13 14:04:10 -05:00
|
|
|
# The user specified a single layoutString (the "edit" one)
|
|
|
|
layouts = {'edit': layouts}
|
2010-10-29 07:36:36 -05:00
|
|
|
elif isinstance(layouts, Table):
|
|
|
|
# Idem, but with a Table instance
|
|
|
|
layouts = {'edit': Table(other=layouts)}
|
2010-09-13 14:04:10 -05:00
|
|
|
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.
|
2010-11-22 08:34:04 -06:00
|
|
|
layouts = copy.deepcopy(layouts)
|
|
|
|
if 'edit' not in layouts:
|
|
|
|
defEditLayout = self.computeDefaultLayouts()
|
|
|
|
if type(defEditLayout) == dict:
|
|
|
|
defEditLayout = defEditLayout['edit']
|
|
|
|
layouts['edit'] = defEditLayout
|
2010-09-13 14:04:10 -05:00
|
|
|
# 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.
|
2010-08-05 11:23:17 -05:00
|
|
|
for layoutType in layouts.iterkeys():
|
|
|
|
if isinstance(layouts[layoutType], basestring):
|
|
|
|
layouts[layoutType] = Table(layouts[layoutType])
|
2010-11-22 08:34:04 -06:00
|
|
|
# Derive "view" and "cell" layouts from the "edit" layout when relevant
|
2010-09-13 14:04:10 -05:00
|
|
|
if 'view' not in layouts:
|
|
|
|
layouts['view'] = Table(other=layouts['edit'], derivedType='view')
|
|
|
|
# Create the "cell" layout from the 'view' layout if not specified.
|
2010-08-05 11:23:17 -05:00
|
|
|
if 'cell' not in layouts:
|
2010-09-13 14:04:10 -05:00
|
|
|
layouts['cell'] = Table(other=layouts['view'], derivedType='cell')
|
2010-08-05 11:23:17 -05:00
|
|
|
# Put the required CSS classes in the layouts
|
2011-09-18 08:00:05 -05:00
|
|
|
layouts['cell'].addCssClasses('noStyle')
|
2010-08-05 11:23:17 -05:00
|
|
|
if self.focus:
|
|
|
|
# We need to make it flashy
|
2012-06-02 13:55:25 -05:00
|
|
|
layouts['view'].addCssClasses('focus')
|
|
|
|
layouts['edit'].addCssClasses('focus')
|
2010-08-05 11:23:17 -05:00
|
|
|
# If layouts are the default ones, set width=None instead of width=100%
|
2013-01-08 09:58:29 -06:00
|
|
|
# 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 == String.XHTML)):
|
2010-08-05 11:23:17 -05:00
|
|
|
for layoutType in layouts.iterkeys():
|
|
|
|
layouts[layoutType].width = ''
|
|
|
|
# Remove letters "r" from the layouts if the field is not required.
|
|
|
|
if not self.required:
|
|
|
|
for layoutType in layouts.iterkeys():
|
|
|
|
layouts[layoutType].removeElement('r')
|
2010-09-13 14:04:10 -05:00
|
|
|
# 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)
|
2010-08-05 11:23:17 -05:00
|
|
|
# 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
|
|
|
|
|
2010-09-13 14:04:10 -05:00
|
|
|
def hasLayoutElement(self, element, layouts):
|
2010-08-05 11:23:17 -05:00
|
|
|
'''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.'''
|
2010-09-13 14:04:10 -05:00
|
|
|
for layout in layouts.itervalues():
|
|
|
|
if element in layout.layoutString: return True
|
2010-08-05 11:23:17 -05:00
|
|
|
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.'''
|
|
|
|
|
2011-01-14 02:06:25 -06:00
|
|
|
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
|
|
|
|
|
2010-11-22 08:34:04 -06:00
|
|
|
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
|
|
|
|
|
2012-05-05 10:04:19 -05:00
|
|
|
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)
|
2010-08-05 11:23:17 -05:00
|
|
|
|
|
|
|
def getValue(self, obj):
|
|
|
|
'''Gets, on_obj, the value conforming to self's type definition.'''
|
2011-10-11 10:32:23 -05:00
|
|
|
value = getattr(obj.aq_base, self.name, None)
|
2011-10-26 03:21:09 -05:00
|
|
|
if self.isEmptyValue(value):
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
# 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
|
2010-08-05 11:23:17 -05:00
|
|
|
return value
|
|
|
|
|
2013-01-08 09:58:29 -06:00
|
|
|
def getFormattedValue(self, obj, value, showChanges=False):
|
2010-08-05 11:23:17 -05:00
|
|
|
'''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
|
2013-01-08 09:58:29 -06:00
|
|
|
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.'''
|
2010-11-10 08:15:00 -06:00
|
|
|
if self.isEmptyValue(value): return ''
|
2010-08-05 11:23:17 -05:00
|
|
|
return value
|
|
|
|
|
2010-12-17 07:46:55 -06:00
|
|
|
def getIndexType(self):
|
|
|
|
'''Returns the name of the technical, Zope-level index type for this
|
|
|
|
field.'''
|
2012-07-18 14:58:11 -05:00
|
|
|
# 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
|
2010-12-17 07:46:55 -06:00
|
|
|
return 'FieldIndex'
|
|
|
|
|
2010-11-30 10:41:18 -06:00
|
|
|
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
|
|
|
|
|
2012-03-01 10:35:23 -06:00
|
|
|
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)
|
2010-08-05 11:23:17 -05:00
|
|
|
|
|
|
|
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.'''
|
2010-11-10 08:15:00 -06:00
|
|
|
if self.isEmptyValue(value): return None
|
2010-08-05 11:23:17 -05:00
|
|
|
return value
|
|
|
|
|
2010-11-05 07:05:10 -05:00
|
|
|
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()
|
|
|
|
|
2010-11-10 08:15:00 -06:00
|
|
|
def isEmptyValue(self, value, obj=None):
|
|
|
|
'''Returns True if the p_value must be considered as an empty value.'''
|
|
|
|
return value in nullValues
|
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
def validateValue(self, obj, value):
|
|
|
|
'''This method may be overridden by child classes and will be called at
|
|
|
|
the right moment by m_validate defined below for triggering
|
|
|
|
type-specific validation. p_value is never empty.'''
|
|
|
|
return None
|
|
|
|
|
2011-05-05 09:44:06 -05:00
|
|
|
def securityCheck(self, obj, value):
|
|
|
|
'''This method performs some security checks on the p_value that
|
|
|
|
represents user input.'''
|
|
|
|
if not isinstance(value, basestring): return
|
|
|
|
# Search Javascript code in the value (prevent XSS attacks).
|
|
|
|
if '<script' in value:
|
|
|
|
obj.log('Detected Javascript in user input.', type='error')
|
2012-07-26 10:22:22 -05:00
|
|
|
raise Exception('Your behaviour is considered a security ' \
|
|
|
|
'attack. System administrator has been warned.')
|
2011-05-05 09:44:06 -05:00
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
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.
|
2010-11-10 08:15:00 -06:00
|
|
|
if self.isEmptyValue(value, obj):
|
2010-11-05 07:05:10 -05:00
|
|
|
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
|
2011-05-05 09:44:06 -05:00
|
|
|
# Perform security checks on p_value
|
|
|
|
self.securityCheck(obj, value)
|
2010-08-05 11:23:17 -05:00
|
|
|
# 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()
|
2010-09-02 09:16:08 -05:00
|
|
|
if type(self.validator) != validatorTypes[-1]:
|
2010-08-05 11:23:17 -05:00
|
|
|
# It is a custom function. Execute it.
|
|
|
|
try:
|
|
|
|
validValue = self.validator(obj, value)
|
|
|
|
if isinstance(validValue, basestring) and validValue:
|
|
|
|
# Validation failed; and p_validValue contains an error
|
|
|
|
# message.
|
|
|
|
return validValue
|
|
|
|
else:
|
|
|
|
if not validValue:
|
2010-11-16 08:32:47 -06:00
|
|
|
return obj.translate('field_invalid')
|
2010-08-05 11:23:17 -05:00
|
|
|
except Exception, e:
|
|
|
|
return str(e)
|
|
|
|
except:
|
2010-11-16 08:32:47 -06:00
|
|
|
return obj.translate('field_invalid')
|
2010-09-02 09:16:08 -05:00
|
|
|
else:
|
2010-08-05 11:23:17 -05:00
|
|
|
# It is a regular expression
|
2010-08-12 04:56:42 -05:00
|
|
|
if not self.validator.match(value):
|
2010-08-05 11:23:17 -05:00
|
|
|
# If the regular expression is among the default ones, we
|
|
|
|
# generate a specific error message.
|
2010-08-12 04:56:42 -05:00
|
|
|
if self.validator == String.EMAIL:
|
2010-08-05 11:23:17 -05:00
|
|
|
return obj.translate('bad_email')
|
2010-08-12 04:56:42 -05:00
|
|
|
elif self.validator == String.URL:
|
2010-08-05 11:23:17 -05:00
|
|
|
return obj.translate('bad_url')
|
2010-08-12 04:56:42 -05:00
|
|
|
elif self.validator == String.ALPHANUMERIC:
|
2010-08-05 11:23:17 -05:00
|
|
|
return obj.translate('bad_alphanumeric')
|
|
|
|
else:
|
2010-11-16 08:32:47 -06:00
|
|
|
return obj.translate('field_invalid')
|
2010-08-05 11:23:17 -05:00
|
|
|
|
|
|
|
def store(self, obj, value):
|
|
|
|
'''Stores the p_value (produced by m_getStorableValue) that complies to
|
|
|
|
p_self type definition on p_obj.'''
|
|
|
|
setattr(obj, self.name, value)
|
|
|
|
|
2011-03-04 03:30:45 -06:00
|
|
|
def callMethod(self, obj, method, raiseOnError=True):
|
2011-01-14 02:06:25 -06:00
|
|
|
'''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.
|
2012-08-07 02:46:27 -05:00
|
|
|
tb = Traceback.get()
|
2011-01-14 02:06:25 -06:00
|
|
|
try:
|
|
|
|
return method(obj, self)
|
|
|
|
except Exception, e:
|
2012-09-10 04:44:22 -05:00
|
|
|
obj.log(tb, type='error')
|
2012-09-04 11:00:22 -05:00
|
|
|
if raiseOnError:
|
|
|
|
# Raise the initial error.
|
|
|
|
raise te
|
|
|
|
else:
|
|
|
|
return str(te)
|
2011-01-14 02:06:25 -06:00
|
|
|
except Exception, e:
|
2012-09-10 04:44:22 -05:00
|
|
|
obj.log(Traceback.get(), type='error')
|
|
|
|
if raiseOnError:
|
|
|
|
raise e
|
2012-09-04 11:00:22 -05:00
|
|
|
else:
|
|
|
|
return str(e)
|
2011-01-14 02:06:25 -06:00
|
|
|
|
2012-07-26 10:22:22 -05:00
|
|
|
def process(self, obj):
|
|
|
|
'''This method is a general hook allowing a field to perform some
|
|
|
|
processing after an URL on an object has been called, of the form
|
|
|
|
<objUrl>/onProcess.'''
|
|
|
|
return obj.goto(obj.absolute_url())
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
class Integer(Type):
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
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,
|
2013-02-19 02:57:02 -06:00
|
|
|
specificWritePermission=False, width=5, height=None,
|
2011-05-05 09:44:06 -05:00
|
|
|
maxChars=13, colspan=1, master=None, masterValue=None,
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
focus=False, historized=False, mapping=None, label=None,
|
2013-02-19 02:57:02 -06:00
|
|
|
sdefault=('',''), scolspan=1, swidth=None, sheight=None):
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
Type.__init__(self, validator, multiplicity, default, show, page, group,
|
|
|
|
layouts, move, indexed, searchable,specificReadPermission,
|
2011-05-05 09:44:06 -05:00
|
|
|
specificWritePermission, width, height, maxChars, colspan,
|
2011-09-06 14:46:57 -05:00
|
|
|
master, masterValue, focus, historized, True, mapping,
|
2013-02-19 02:57:02 -06:00
|
|
|
label, sdefault, scolspan, swidth, sheight)
|
2009-06-29 07:06:01 -05:00
|
|
|
self.pythonType = long
|
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
def validateValue(self, obj, value):
|
|
|
|
try:
|
|
|
|
value = self.pythonType(value)
|
|
|
|
except ValueError:
|
|
|
|
return obj.translate('bad_%s' % self.pythonType.__name__)
|
|
|
|
|
|
|
|
def getStorableValue(self, value):
|
2010-11-10 08:15:00 -06:00
|
|
|
if not self.isEmptyValue(value): return self.pythonType(value)
|
|
|
|
|
2013-01-08 09:58:29 -06:00
|
|
|
def getFormattedValue(self, obj, value, showChanges=False):
|
2010-11-10 08:15:00 -06:00
|
|
|
if self.isEmptyValue(value): return ''
|
|
|
|
return str(value)
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
class Float(Type):
|
2010-09-15 03:38:35 -05:00
|
|
|
allowedDecimalSeps = (',', '.')
|
2011-11-10 14:59:02 -06:00
|
|
|
allowedThousandsSeps = (' ', '')
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
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,
|
2013-02-19 02:57:02 -06:00
|
|
|
specificWritePermission=False, width=5, height=None,
|
2011-05-05 09:44:06 -05:00
|
|
|
maxChars=13, colspan=1, master=None, masterValue=None,
|
2011-09-06 14:46:57 -05:00
|
|
|
focus=False, historized=False, mapping=None, label=None,
|
2013-02-19 02:57:02 -06:00
|
|
|
sdefault=('',''), scolspan=1, swidth=None, sheight=None,
|
|
|
|
precision=None, sep=(',', '.'), tsep=' '):
|
2010-01-29 04:28:39 -06:00
|
|
|
# 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
|
2010-09-15 03:38:35 -05:00
|
|
|
# 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:
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
raise Exception('Char "%s" is not allowed as decimal ' \
|
|
|
|
'separator.' % sep)
|
2011-11-10 14:59:02 -06:00
|
|
|
self.tsep = tsep
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
Type.__init__(self, validator, multiplicity, default, show, page, group,
|
|
|
|
layouts, move, indexed, False, specificReadPermission,
|
|
|
|
specificWritePermission, width, height, maxChars, colspan,
|
|
|
|
master, masterValue, focus, historized, True, mapping,
|
2013-02-19 02:57:02 -06:00
|
|
|
label, sdefault, scolspan, swidth, sheight)
|
2010-08-05 11:23:17 -05:00
|
|
|
self.pythonType = float
|
|
|
|
|
2013-01-08 09:58:29 -06:00
|
|
|
def getFormattedValue(self, obj, value, showChanges=False):
|
2011-11-10 14:59:02 -06:00
|
|
|
return formatNumber(value, sep=self.sep[0], precision=self.precision,
|
|
|
|
tsep=self.tsep)
|
2010-08-05 11:23:17 -05:00
|
|
|
|
|
|
|
def validateValue(self, obj, value):
|
2010-09-15 03:38:35 -05:00
|
|
|
# Replace used separator with the Python separator '.'
|
|
|
|
for sep in self.sep: value = value.replace(sep, '.')
|
2011-11-10 14:59:02 -06:00
|
|
|
value = value.replace(self.tsep, '')
|
2010-08-05 11:23:17 -05:00
|
|
|
try:
|
|
|
|
value = self.pythonType(value)
|
|
|
|
except ValueError:
|
|
|
|
return obj.translate('bad_%s' % self.pythonType.__name__)
|
|
|
|
|
|
|
|
def getStorableValue(self, value):
|
2010-11-10 08:15:00 -06:00
|
|
|
if not self.isEmptyValue(value):
|
2010-09-15 03:38:35 -05:00
|
|
|
for sep in self.sep: value = value.replace(sep, '.')
|
2011-11-10 14:59:02 -06:00
|
|
|
value = value.replace(self.tsep, '')
|
2010-09-15 03:38:35 -05:00
|
|
|
return self.pythonType(value)
|
2009-06-29 07:06:01 -05:00
|
|
|
|
|
|
|
class String(Type):
|
2012-05-05 10:04:19 -05:00
|
|
|
# Javascript files sometimes required by this type
|
2013-03-29 08:50:12 -05:00
|
|
|
jsFiles = {'edit': ('ckeditor/ckeditor.js',),
|
|
|
|
'view': ('ckeditor/ckeditor.js',)}
|
2012-05-05 10:04:19 -05:00
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
# 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})?\/.*)?')
|
2009-10-27 08:48:04 -05:00
|
|
|
|
|
|
|
# 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
|
2009-11-03 08:02:18 -06:00
|
|
|
specified as parameter. The function returns True if the check is
|
|
|
|
successful.'''
|
2011-12-05 08:11:29 -06:00
|
|
|
if not value: return True
|
2009-10-27 08:48:04 -05:00
|
|
|
# First, remove any non-digit char
|
|
|
|
v = ''
|
|
|
|
for c in value:
|
2009-11-17 08:46:41 -06:00
|
|
|
if digit.match(c): v += c
|
2009-10-27 08:48:04 -05:00
|
|
|
# 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:
|
2010-01-07 13:25:18 -06:00
|
|
|
# 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
|
2009-10-27 08:48:04 -05:00
|
|
|
@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)
|
2010-08-05 11:23:17 -05:00
|
|
|
BELGIAN_ENTERPRISE_NUMBER = MODULO_97_COMPLEMENT
|
2009-11-17 08:46:41 -06:00
|
|
|
@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.'''
|
2011-12-05 08:11:29 -06:00
|
|
|
if not value: return True
|
2009-11-17 08:46:41 -06:00
|
|
|
# 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
|
2011-04-26 13:49:33 -05:00
|
|
|
if not countries.exists(v[:2].upper()): return False
|
2009-11-17 08:46:41 -06:00
|
|
|
# 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.'''
|
2011-12-05 08:11:29 -06:00
|
|
|
if not value: return True
|
2009-11-17 08:46:41 -06:00
|
|
|
# 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
|
2011-04-26 13:49:33 -05:00
|
|
|
if not countries.exists(value[4:6].upper()): return False
|
2009-11-17 08:46:41 -06:00
|
|
|
# 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
|
2009-10-27 08:48:04 -05:00
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
# Possible values for "format"
|
|
|
|
LINE = 0
|
|
|
|
TEXT = 1
|
|
|
|
XHTML = 2
|
2009-11-25 13:37:22 -06:00
|
|
|
PASSWORD = 3
|
2012-02-16 11:13:51 -06:00
|
|
|
CAPTCHA = 4
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
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,
|
2013-02-19 02:57:02 -06:00
|
|
|
label=None, sdefault='', scolspan=1, swidth=None, sheight=None,
|
2012-12-14 02:23:33 -06:00
|
|
|
transform='none', styles=('p','h1','h2','h3','h4'),
|
2013-03-29 08:50:12 -05:00
|
|
|
allowImageUpload=True, inlineEdit=False):
|
2012-01-09 10:00:47 -06:00
|
|
|
# According to format, the widget will be different: input field,
|
2012-02-16 11:13:51 -06:00
|
|
|
# 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.
|
2009-06-29 07:06:01 -05:00
|
|
|
self.format = format
|
2012-10-31 15:17:31 -05:00
|
|
|
self.isUrl = validator == String.URL
|
2012-01-09 10:00:47 -06:00
|
|
|
# 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
|
2013-03-29 08:50:12 -05:00
|
|
|
# When format in XHTML, can the field be inline-edited (ckeditor)?
|
|
|
|
self.inlineEdit = inlineEdit
|
2010-04-16 10:07:34 -05:00
|
|
|
# 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
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
Type.__init__(self, validator, multiplicity, default, show, page, group,
|
|
|
|
layouts, move, indexed, searchable,specificReadPermission,
|
2011-05-05 09:44:06 -05:00
|
|
|
specificWritePermission, width, height, maxChars, colspan,
|
2011-09-06 14:46:57 -05:00
|
|
|
master, masterValue, focus, historized, True, mapping,
|
2013-02-19 02:57:02 -06:00
|
|
|
label, sdefault, scolspan, swidth, sheight)
|
2010-08-05 11:23:17 -05:00
|
|
|
self.isSelect = self.isSelection()
|
2012-12-14 02:23:33 -06:00
|
|
|
# If self.isSelect, self.sdefault must be a list of value(s).
|
|
|
|
if self.isSelect and not sdefault:
|
|
|
|
self.sdefault = []
|
2011-05-05 09:44:06 -05:00
|
|
|
# Default width, height and maxChars vary according to String format
|
2010-08-12 04:56:42 -05:00
|
|
|
if width == None:
|
2012-09-10 04:44:22 -05:00
|
|
|
if format == String.TEXT: self.width = 60
|
2012-11-29 13:45:21 -06:00
|
|
|
# This width corresponds to the standard width of an Appy page.
|
2013-04-11 09:01:52 -05:00
|
|
|
if format == String.XHTML: self.width = None
|
2012-09-10 04:44:22 -05:00
|
|
|
else: self.width = 30
|
2010-08-12 04:56:42 -05:00
|
|
|
if height == None:
|
|
|
|
if format == String.TEXT: self.height = 5
|
|
|
|
elif self.isSelect: self.height = 4
|
|
|
|
else: self.height = 1
|
2011-05-05 09:44:06 -05:00
|
|
|
if maxChars == None:
|
|
|
|
if self.isSelect: pass
|
|
|
|
elif format == String.LINE: self.maxChars = 256
|
|
|
|
elif format == String.TEXT: self.maxChars = 9999
|
2012-04-24 09:22:12 -05:00
|
|
|
elif format == String.XHTML: self.maxChars = 99999
|
2011-05-05 09:44:06 -05:00
|
|
|
elif format == String.PASSWORD: self.maxChars = 20
|
2010-08-05 11:23:17 -05:00
|
|
|
self.filterable = self.indexed and (self.format == String.LINE) and \
|
|
|
|
not self.isSelect
|
2013-02-19 02:57:02 -06:00
|
|
|
self.swidth = self.swidth or self.width
|
|
|
|
self.sheight = self.sheight or self.height
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
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
|
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
def getDefaultLayouts(self):
|
|
|
|
'''Returns the default layouts for this type. Default layouts can vary
|
2013-01-08 06:28:35 -06:00
|
|
|
acccording to format, multiplicity or history.'''
|
|
|
|
if self.format == String.TEXT:
|
2011-10-06 09:27:58 -05:00
|
|
|
return {'view': 'l-f', 'edit': 'lrv-d-f'}
|
2013-01-08 06:28:35 -06:00
|
|
|
elif self.format == String.XHTML:
|
|
|
|
if self.historized:
|
2013-01-10 03:56:22 -06:00
|
|
|
# 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.
|
2013-01-08 06:28:35 -06:00
|
|
|
view = 'lc-f'
|
|
|
|
else:
|
|
|
|
view = 'l-f'
|
2013-01-08 09:58:29 -06:00
|
|
|
return {'view': Table(view, width='100%'), 'edit': 'lrv-d-f'}
|
2010-08-05 11:23:17 -05:00
|
|
|
elif self.isMultiValued():
|
|
|
|
return {'view': 'l-f', 'edit': 'lrv-f'}
|
|
|
|
|
|
|
|
def getValue(self, obj):
|
2012-10-31 15:17:31 -05:00
|
|
|
# Cheat if this field represents p_obj's state
|
|
|
|
if self.name == 'state': return obj.State()
|
2010-08-05 11:23:17 -05:00
|
|
|
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]
|
2010-11-30 10:41:18 -06:00
|
|
|
elif isinstance(value, tuple):
|
|
|
|
value = list(value)
|
2010-08-05 11:23:17 -05:00
|
|
|
return value
|
|
|
|
|
2012-04-24 09:22:12 -05:00
|
|
|
def store(self, obj, value):
|
|
|
|
'''When the value is XHTML, we perform some cleanup.'''
|
|
|
|
if (self.format == String.XHTML) and value:
|
2012-04-25 09:21:23 -05:00
|
|
|
# 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.
|
2012-05-22 09:42:20 -05:00
|
|
|
try:
|
2013-03-25 10:38:52 -05:00
|
|
|
value = XhtmlCleaner(keepStyles=False).clean(value)
|
2012-05-22 09:42:20 -05:00
|
|
|
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')
|
2012-04-24 09:22:12 -05:00
|
|
|
Type.store(self, obj, value)
|
|
|
|
|
2013-01-08 09:58:29 -06:00
|
|
|
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):
|
2010-11-10 08:15:00 -06:00
|
|
|
if self.isEmptyValue(value): return ''
|
2010-08-05 11:23:17 -05:00
|
|
|
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))
|
2013-01-08 09:58:29 -06:00
|
|
|
elif (self.format == String.XHTML) and showChanges:
|
|
|
|
# Compute the successive changes that occurred on p_value.
|
|
|
|
res = self.getDiffValue(obj, res)
|
2011-01-14 02:06:25 -06:00
|
|
|
# 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
|
2010-08-05 11:23:17 -05:00
|
|
|
return res
|
|
|
|
|
2011-01-17 07:49:56 -06:00
|
|
|
emptyStringTuple = ('',)
|
2011-02-22 07:16:42 -06:00
|
|
|
emptyValuesCatalogIgnored = (None, '')
|
2010-12-17 07:46:55 -06:00
|
|
|
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')
|
2012-10-05 09:38:15 -05:00
|
|
|
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)
|
2011-11-25 11:01:20 -06:00
|
|
|
# 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.
|
2011-01-17 07:49:56 -06:00
|
|
|
if isinstance(res, tuple) and not res: res = self.emptyStringTuple
|
2011-11-25 11:01:20 -06:00
|
|
|
# Ugly catalog: if value is an empty string or None, it keeps the
|
|
|
|
# previous index value.
|
2011-02-22 07:16:42 -06:00
|
|
|
if res in self.emptyValuesCatalogIgnored: res = ' '
|
2010-12-17 07:46:55 -06:00
|
|
|
return res
|
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
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):
|
2012-02-16 11:13:51 -06:00
|
|
|
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')
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2010-11-23 10:25:00 -06:00
|
|
|
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):
|
2012-03-08 13:56:14 -06:00
|
|
|
isString = isinstance(value, basestring)
|
|
|
|
# Apply transform if required
|
|
|
|
if isString and not self.isEmptyValue(value) and \
|
|
|
|
(self.transform != 'none'):
|
|
|
|
value = self.applyTransform(value)
|
2011-05-05 09:44:06 -05:00
|
|
|
# Truncate the result if longer than self.maxChars
|
2012-03-08 13:56:14 -06:00
|
|
|
if isString and self.maxChars and (len(value) > self.maxChars):
|
2011-05-05 09:44:06 -05:00
|
|
|
value = value[:self.maxChars]
|
2012-03-08 13:56:14 -06:00
|
|
|
# Get a multivalued value if required.
|
|
|
|
if value and self.isMultiValued() and \
|
|
|
|
(type(value) not in sequenceTypes):
|
|
|
|
value = [value]
|
|
|
|
return value
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2010-12-17 07:46:55 -06:00
|
|
|
def getIndexType(self):
|
|
|
|
'''Index type varies depending on String parameters.'''
|
2012-09-26 16:13:02 -05:00
|
|
|
# If String.isSelect, be it multivalued or not, we define a ListIndex:
|
2010-12-17 07:46:55 -06:00
|
|
|
# this way we can use AND/OR operator.
|
2012-09-26 16:13:02 -05:00
|
|
|
if self.isSelect:
|
|
|
|
return 'ListIndex'
|
|
|
|
elif self.format == String.TEXT:
|
|
|
|
return 'TextIndex'
|
|
|
|
elif self.format == String.XHTML:
|
|
|
|
return 'XhtmlIndex'
|
2010-12-17 07:46:55 -06:00
|
|
|
return Type.getIndexType(self)
|
|
|
|
|
2012-05-05 10:04:19 -05:00
|
|
|
def getJs(self, layoutType, res):
|
|
|
|
if self.format == String.XHTML: Type.getJs(self, layoutType, res)
|
2011-09-28 14:17:15 -05:00
|
|
|
|
2012-02-16 11:13:51 -06:00
|
|
|
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)
|
|
|
|
if j == 0:
|
|
|
|
chars = string.digits
|
|
|
|
else:
|
|
|
|
chars = string.letters
|
|
|
|
# Choose a char
|
|
|
|
text += chars[random.randint(0,len(chars)-1)]
|
|
|
|
res = {'text': text, 'number': number}
|
|
|
|
session['captcha'] = res
|
|
|
|
return res
|
|
|
|
|
2012-04-25 09:21:23 -05:00
|
|
|
def generatePassword(self):
|
|
|
|
'''Generates a password (we recycle here the captcha challenge
|
|
|
|
generator).'''
|
|
|
|
return self.getCaptchaChallenge({})['text']
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
class Boolean(Type):
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
'''Field for storing boolean values.'''
|
|
|
|
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,
|
2009-10-30 15:31:39 -05:00
|
|
|
specificWritePermission=False, width=None, height=None,
|
2011-05-05 09:44:06 -05:00
|
|
|
maxChars=None, colspan=1, master=None, masterValue=None,
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
focus=False, historized=False, mapping=None, label=None,
|
2013-02-19 02:57:02 -06:00
|
|
|
sdefault=False, scolspan=1, swidth=None, sheight=None):
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
Type.__init__(self, validator, multiplicity, default, show, page, group,
|
|
|
|
layouts, move, indexed, searchable,specificReadPermission,
|
2011-05-05 09:44:06 -05:00
|
|
|
specificWritePermission, width, height, None, colspan,
|
2011-09-06 14:46:57 -05:00
|
|
|
master, masterValue, focus, historized, True, mapping,
|
2013-02-19 02:57:02 -06:00
|
|
|
label, sdefault, scolspan, swidth, sheight)
|
2009-06-29 07:06:01 -05:00
|
|
|
self.pythonType = bool
|
|
|
|
|
2012-12-15 16:36:56 -06:00
|
|
|
# Layout including a description
|
2012-05-31 10:29:06 -05:00
|
|
|
dLayouts = {'view': 'lf', 'edit': Table('flrv;=d', width=None)}
|
2012-12-15 16:36:56 -06:00
|
|
|
# Centered layout, no description
|
|
|
|
cLayouts = {'view': 'lf|', 'edit': 'flrv|'}
|
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
def getDefaultLayouts(self):
|
2012-05-31 10:29:06 -05:00
|
|
|
return {'view': 'lf', 'edit': Table('f;lrv;=', width=None)}
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2011-01-28 19:18:14 -06:00
|
|
|
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
|
|
|
|
|
2013-01-08 09:58:29 -06:00
|
|
|
def getFormattedValue(self, obj, value, showChanges=False):
|
2011-12-05 08:11:29 -06:00
|
|
|
if value: res = obj.translate('yes')
|
|
|
|
else: res = obj.translate('no')
|
2010-08-05 11:23:17 -05:00
|
|
|
return res
|
|
|
|
|
|
|
|
def getStorableValue(self, value):
|
2010-11-10 08:15:00 -06:00
|
|
|
if not self.isEmptyValue(value):
|
2010-08-05 11:23:17 -05:00
|
|
|
exec 'res = %s' % value
|
|
|
|
return res
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
class Date(Type):
|
2012-05-05 10:04:19 -05:00
|
|
|
# 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')}
|
2009-06-29 07:06:01 -05:00
|
|
|
# Possible values for "format"
|
|
|
|
WITH_HOUR = 0
|
|
|
|
WITHOUT_HOUR = 1
|
2010-08-05 11:23:17 -05:00
|
|
|
dateParts = ('year', 'month', 'day')
|
|
|
|
hourParts = ('hour', 'minute')
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
def __init__(self, validator=None, multiplicity=(0,1), default=None,
|
2010-08-27 01:59:53 -05:00
|
|
|
format=WITH_HOUR, calendar=True,
|
|
|
|
startYear=time.localtime()[0]-10,
|
2011-03-25 12:03:45 -05:00
|
|
|
endYear=time.localtime()[0]+10, reverseYears=False,
|
|
|
|
show=True, page='main', group=None, layouts=None, move=0,
|
|
|
|
indexed=False, searchable=False, specificReadPermission=False,
|
2010-08-05 11:23:17 -05:00
|
|
|
specificWritePermission=False, width=None, height=None,
|
2011-05-05 09:44:06 -05:00
|
|
|
maxChars=None, colspan=1, master=None, masterValue=None,
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
focus=False, historized=False, mapping=None, label=None,
|
2013-02-19 02:57:02 -06:00
|
|
|
sdefault=None, scolspan=1, swidth=None, sheight=None):
|
2009-06-29 07:06:01 -05:00
|
|
|
self.format = format
|
2010-08-27 01:59:53 -05:00
|
|
|
self.calendar = calendar
|
2009-10-25 15:42:08 -05:00
|
|
|
self.startYear = startYear
|
|
|
|
self.endYear = endYear
|
2011-03-25 12:03:45 -05:00
|
|
|
# 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
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
Type.__init__(self, validator, multiplicity, default, show, page, group,
|
|
|
|
layouts, move, indexed, searchable,specificReadPermission,
|
2011-05-05 09:44:06 -05:00
|
|
|
specificWritePermission, width, height, None, colspan,
|
2011-09-06 14:46:57 -05:00
|
|
|
master, masterValue, focus, historized, True, mapping,
|
2013-02-19 02:57:02 -06:00
|
|
|
label, sdefault, scolspan, swidth, sheight)
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2012-05-05 10:04:19 -05:00
|
|
|
def getCss(self, layoutType, res):
|
|
|
|
# CSS files are only required if the calendar must be shown.
|
|
|
|
if self.calendar: Type.getCss(self, layoutType, res)
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2012-05-05 10:04:19 -05:00
|
|
|
def getJs(self, layoutType, res):
|
|
|
|
# Javascript files are only required if the calendar must be shown.
|
|
|
|
if self.calendar: Type.getJs(self, layoutType, res)
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2011-03-25 12:03:45 -05:00
|
|
|
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
|
|
|
|
|
2011-02-12 10:09:11 -06:00
|
|
|
def validateValue(self, obj, value):
|
|
|
|
DateTime = obj.getProductConfig().DateTime
|
|
|
|
try:
|
|
|
|
value = DateTime(value)
|
|
|
|
except DateTime.DateError, ValueError:
|
|
|
|
return obj.translate('bad_date')
|
|
|
|
|
2013-01-08 09:58:29 -06:00
|
|
|
def getFormattedValue(self, obj, value, showChanges=False):
|
2010-11-10 08:15:00 -06:00
|
|
|
if self.isEmptyValue(value): return ''
|
2012-11-05 03:21:27 -06:00
|
|
|
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)
|
2010-08-05 11:23:17 -05:00
|
|
|
if self.format == Date.WITH_HOUR:
|
2012-11-05 03:21:27 -06:00
|
|
|
res += ' %s' % value.strftime(tool.hourFormat)
|
2010-08-05 11:23:17 -05:00
|
|
|
return res
|
|
|
|
|
2012-03-01 10:35:23 -06:00
|
|
|
def getRequestValue(self, request, requestName=None):
|
|
|
|
name = requestName or self.name
|
2010-08-05 11:23:17 -05:00
|
|
|
# Manage the "date" part
|
|
|
|
value = ''
|
|
|
|
for part in self.dateParts:
|
2012-03-01 10:35:23 -06:00
|
|
|
valuePart = request.get('%s_%s' % (name, part), None)
|
2010-08-05 11:23:17 -05:00
|
|
|
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:
|
2012-03-01 10:35:23 -06:00
|
|
|
valuePart = request.get('%s_%s' % (name, part), None)
|
2010-08-05 11:23:17 -05:00
|
|
|
if not valuePart: return None
|
|
|
|
value += valuePart + ':'
|
|
|
|
value = value[:-1]
|
|
|
|
return value
|
|
|
|
|
|
|
|
def getStorableValue(self, value):
|
2010-11-10 08:15:00 -06:00
|
|
|
if not self.isEmptyValue(value):
|
2010-08-05 11:23:17 -05:00
|
|
|
import DateTime
|
|
|
|
return DateTime.DateTime(value)
|
2009-06-29 07:06:01 -05:00
|
|
|
|
|
|
|
class File(Type):
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
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,
|
2009-10-30 15:31:39 -05:00
|
|
|
specificWritePermission=False, width=None, height=None,
|
2011-05-05 09:44:06 -05:00
|
|
|
maxChars=None, colspan=1, master=None, masterValue=None,
|
2011-09-06 14:46:57 -05:00
|
|
|
focus=False, historized=False, mapping=None, label=None,
|
2013-02-19 02:57:02 -06:00
|
|
|
isImage=False, sdefault='', scolspan=1, swidth=None,
|
|
|
|
sheight=None):
|
2009-06-29 07:06:01 -05:00
|
|
|
self.isImage = isImage
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
Type.__init__(self, validator, multiplicity, default, show, page, group,
|
|
|
|
layouts, move, indexed, False, specificReadPermission,
|
|
|
|
specificWritePermission, width, height, None, colspan,
|
|
|
|
master, masterValue, focus, historized, True, mapping,
|
2013-02-19 02:57:02 -06:00
|
|
|
label, sdefault, scolspan, swidth, sheight)
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2011-02-16 06:43:58 -06:00
|
|
|
@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 = FileWrapper(res)
|
|
|
|
return res
|
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
def getValue(self, obj):
|
|
|
|
value = Type.getValue(self, obj)
|
|
|
|
if value: value = FileWrapper(value)
|
|
|
|
return value
|
|
|
|
|
2013-01-08 09:58:29 -06:00
|
|
|
def getFormattedValue(self, obj, value, showChanges=False):
|
2010-08-05 11:23:17 -05:00
|
|
|
if not value: return value
|
2011-12-05 08:11:29 -06:00
|
|
|
return value._zopeFile
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2012-03-01 10:35:23 -06:00
|
|
|
def getRequestValue(self, request, requestName=None):
|
|
|
|
name = requestName or self.name
|
|
|
|
return request.get('%s_file' % name)
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2011-12-15 15:56:53 -06:00
|
|
|
def getDefaultLayouts(self): return {'view':'l-f','edit':'lrv-f'}
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2010-11-10 08:15:00 -06:00
|
|
|
def isEmptyValue(self, value, obj=None):
|
|
|
|
'''Must p_value be considered as empty?'''
|
|
|
|
if not obj: return Type.isEmptyValue(self, value)
|
2011-09-20 12:21:48 -05:00
|
|
|
if value: return False
|
2010-11-10 08:15:00 -06:00
|
|
|
# If "nochange", the value must not be considered as empty
|
|
|
|
return obj.REQUEST.get('%s_delete' % self.name) != 'nochange'
|
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
imageExts = ('.jpg', '.jpeg', '.png', '.gif')
|
|
|
|
def validateValue(self, obj, value):
|
|
|
|
form = obj.REQUEST.form
|
|
|
|
action = '%s_delete' % self.name
|
2010-11-10 08:15:00 -06:00
|
|
|
if (not value or not value.filename) and form.has_key(action) and \
|
|
|
|
not form[action]:
|
2010-08-05 11:23:17 -05:00
|
|
|
# If this key is present but empty, it means that the user selected
|
2010-11-10 08:15:00 -06:00
|
|
|
# "replace the file with a new one". So in this case he must provide
|
2010-08-05 11:23:17 -05:00
|
|
|
# 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):
|
2010-11-26 10:30:46 -06:00
|
|
|
'''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;
|
2011-12-15 15:56:53 -06:00
|
|
|
* an instance of appy.shared.utils.FileWrapper, which wraps an
|
|
|
|
instance of OFS.Image.File and adds useful methods for manipulating
|
|
|
|
it;
|
2010-11-26 10:30:46 -06:00
|
|
|
* 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.
|
|
|
|
'''
|
2010-08-05 11:23:17 -05:00
|
|
|
if value:
|
2010-11-30 10:41:18 -06:00
|
|
|
ZFileUpload = obj.o.getProductConfig().FileUpload
|
|
|
|
OFSImageFile = obj.o.getProductConfig().File
|
2010-11-26 10:30:46 -06:00
|
|
|
if isinstance(value, ZFileUpload):
|
|
|
|
# The file content comes from a HTTP POST.
|
|
|
|
# Retrieve the existing value, or create one if None
|
2011-10-11 10:32:23 -05:00
|
|
|
existingValue = getattr(obj.aq_base, self.name, None)
|
2010-11-26 10:30:46 -06:00
|
|
|
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):
|
2011-12-05 08:11:29 -06:00
|
|
|
setattr(obj, self.name, value._zopeFile)
|
2010-11-26 10:30:46 -06:00
|
|
|
elif isinstance(value, basestring):
|
2011-02-16 06:43:58 -06:00
|
|
|
setattr(obj, self.name, File.getFileObject(value, zope=True))
|
2010-11-26 10:30:46 -06:00
|
|
|
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)
|
2010-08-05 11:23:17 -05:00
|
|
|
else:
|
2010-11-26 10:30:46 -06:00
|
|
|
# I store value "None", excepted if I find in the request the desire
|
|
|
|
# to keep the file unchanged.
|
2012-08-24 09:39:45 -05:00
|
|
|
action = None
|
|
|
|
rq = getattr(obj, 'REQUEST', None)
|
|
|
|
if rq: action = rq.get('%s_delete' % self.name, None)
|
2010-08-05 11:23:17 -05:00
|
|
|
if action == 'nochange': pass
|
|
|
|
else: setattr(obj, self.name, None)
|
2009-06-29 07:06:01 -05:00
|
|
|
|
|
|
|
class Ref(Type):
|
2012-08-21 12:57:23 -05:00
|
|
|
# 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%')}
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
def __init__(self, klass=None, attribute=None, validator=None,
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
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,
|
2013-02-19 02:57:02 -06:00
|
|
|
sdefault='', scolspan=1, swidth=None, sheight=None):
|
2009-06-29 07:06:01 -05:00
|
|
|
self.klass = klass
|
|
|
|
self.attribute = attribute
|
2010-08-05 11:23:17 -05:00
|
|
|
# May the user add new objects through this ref ?
|
|
|
|
self.add = add
|
2010-10-14 07:43:56 -05:00
|
|
|
# When the user adds a new object, must a confirmation popup be shown?
|
|
|
|
self.addConfirm = addConfirm
|
2012-10-08 03:08:54 -05:00
|
|
|
# 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)
|
2010-10-14 07:43:56 -05:00
|
|
|
# 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
|
2010-08-05 11:23:17 -05:00
|
|
|
# May the user link existing objects through this ref?
|
|
|
|
self.link = link
|
|
|
|
# May the user unlink existing objects?
|
|
|
|
self.unlink = unlink
|
2012-10-08 03:08:54 -05:00
|
|
|
if unlink == None:
|
|
|
|
# By default, one may unlink objects via a Ref for which one can
|
|
|
|
# link objects.
|
|
|
|
self.unlink = bool(self.link)
|
2011-09-06 14:46:57 -05:00
|
|
|
self.back = None
|
2010-08-05 11:23:17 -05:00
|
|
|
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__
|
2012-03-06 10:02:41 -06:00
|
|
|
# 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)
|
2010-08-05 11:23:17 -05:00
|
|
|
# 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.
|
2011-09-28 14:17:15 -05:00
|
|
|
self.shownInfo = list(shownInfo)
|
|
|
|
if not self.shownInfo: self.shownInfo.append('title')
|
2010-08-05 11:23:17 -05:00
|
|
|
# If a method is defined in this field "select", it will be used to
|
2009-06-29 07:06:01 -05:00
|
|
|
# filter the list of available tied objects.
|
2010-08-05 11:23:17 -05:00
|
|
|
self.select = select
|
|
|
|
# Maximum number of referenced objects shown at once.
|
2010-09-17 02:27:14 -05:00
|
|
|
self.maxPerPage = maxPerPage
|
|
|
|
# Specifies sync
|
|
|
|
sync = {'view': False, 'edit':True}
|
2010-12-06 04:11:40 -06:00
|
|
|
# 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
|
2012-03-27 03:37:41 -05:00
|
|
|
# Within the portlet, will referred elements appear ?
|
|
|
|
self.navigable = navigable
|
2012-09-17 14:11:54 -05:00
|
|
|
# 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
|
2012-10-07 04:26:55 -05:00
|
|
|
# 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
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
Type.__init__(self, validator, multiplicity, default, show, page, group,
|
|
|
|
layouts, move, indexed, False, specificReadPermission,
|
|
|
|
specificWritePermission, width, height, None, colspan,
|
|
|
|
master, masterValue, focus, historized, sync, mapping,
|
2013-02-19 02:57:02 -06:00
|
|
|
label, sdefault, scolspan, swidth, sheight)
|
2010-08-05 11:23:17 -05:00
|
|
|
self.validable = self.link
|
|
|
|
|
2011-09-18 08:00:05 -05:00
|
|
|
def getDefaultLayouts(self):
|
|
|
|
return {'view': Table('l-f', width='100%'), 'edit': 'lrv-f'}
|
2010-08-12 04:56:42 -05:00
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
def isShowable(self, obj, layoutType):
|
2010-09-13 14:04:10 -05:00
|
|
|
res = Type.isShowable(self, obj, layoutType)
|
2010-08-27 01:59:53 -05:00
|
|
|
if not res: return res
|
2010-09-13 14:04:10 -05:00
|
|
|
# We add here specific Ref rules for preventing to show the field under
|
|
|
|
# some inappropriate circumstances.
|
2012-08-17 10:12:15 -05:00
|
|
|
if (layoutType == 'edit') and \
|
|
|
|
(self.mayAdd(obj) or not self.link): return False
|
2010-08-05 11:23:17 -05:00
|
|
|
if self.isBack:
|
|
|
|
if layoutType == 'edit': return False
|
2011-10-11 10:32:23 -05:00
|
|
|
else: return getattr(obj.aq_base, self.name, None)
|
2010-09-13 14:04:10 -05:00
|
|
|
return res
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2010-10-14 07:43:56 -05:00
|
|
|
def getValue(self, obj, type='objects', noListIfSingleObj=False,
|
|
|
|
startNumber=None, someObjects=False):
|
2011-09-26 14:19:34 -05:00
|
|
|
'''Returns the objects linked to p_obj through this Ref field.
|
2010-10-14 07:43:56 -05:00
|
|
|
- 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.'''
|
2011-10-11 10:32:23 -05:00
|
|
|
uids = getattr(obj.aq_base, self.name, [])
|
2011-01-28 07:36:30 -06:00
|
|
|
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()]
|
2011-09-26 14:19:34 -05:00
|
|
|
# Prepare the result: an instance of SomeObjects, that will be unwrapped
|
|
|
|
# if not required.
|
2010-10-14 07:43:56 -05:00
|
|
|
res = SomeObjects()
|
|
|
|
res.totalNumber = res.batchSize = len(uids)
|
|
|
|
batchNeeded = startNumber != None
|
|
|
|
if batchNeeded:
|
|
|
|
res.batchSize = self.maxPerPage
|
|
|
|
if startNumber != None:
|
|
|
|
res.startNumber = startNumber
|
2011-09-26 14:19:34 -05:00
|
|
|
# Get the objects given their uids
|
2010-10-14 07:43:56 -05:00
|
|
|
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:
|
2011-11-25 11:01:20 -06:00
|
|
|
ref = obj.getTool().getObject(uids[i])
|
2010-10-14 07:43:56 -05:00
|
|
|
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
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2013-01-08 09:58:29 -06:00
|
|
|
def getFormattedValue(self, obj, value, showChanges=False):
|
2010-08-05 11:23:17 -05:00
|
|
|
return value
|
|
|
|
|
2012-09-26 16:32:57 -05:00
|
|
|
def getIndexType(self): return 'ListIndex'
|
2012-09-17 14:11:54 -05:00
|
|
|
|
|
|
|
def getIndexValue(self, obj, forSearch=False):
|
|
|
|
'''Value for indexing is the list of UIDs of linked objects. If
|
2012-09-26 16:32:57 -05:00
|
|
|
p_forSearch is True, it will return a list of the linked objects'
|
|
|
|
titles instead.'''
|
2012-09-17 14:11:54 -05:00
|
|
|
if not forSearch:
|
2012-09-26 16:32:57 -05:00
|
|
|
res = getattr(obj.aq_base, self.name, [])
|
|
|
|
if res:
|
|
|
|
# The index does not like persistent lists.
|
|
|
|
res = list(res)
|
2012-09-17 14:11:54 -05:00
|
|
|
return res
|
|
|
|
else:
|
2012-09-26 16:32:57 -05:00
|
|
|
# For the global search: return linked objects' titles.
|
|
|
|
return [o.title for o in self.getValue(type='objects')]
|
2012-09-17 14:11:54 -05:00
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
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')
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2011-10-04 13:12:58 -05:00
|
|
|
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 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
|
2011-10-11 10:32:23 -05:00
|
|
|
refs = getattr(obj.aq_base, self.name, None)
|
2011-10-04 13:12:58 -05:00
|
|
|
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:
|
2012-05-03 03:51:54 -05:00
|
|
|
# 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)
|
2011-10-04 13:12:58 -05:00
|
|
|
# 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 sequenceTypes:
|
|
|
|
for v in value: self.unlinkObject(obj, v, back=back)
|
|
|
|
return
|
|
|
|
obj = obj.o
|
2011-10-11 10:32:23 -05:00
|
|
|
refs = getattr(obj.aq_base, self.name, None)
|
2011-10-04 13:12:58 -05:00
|
|
|
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)
|
|
|
|
|
2010-10-14 07:43:56 -05:00
|
|
|
def store(self, obj, value):
|
2010-11-26 10:30:46 -06:00
|
|
|
'''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;
|
2011-10-04 13:12:58 -05:00
|
|
|
* 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 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
|
2011-10-11 10:32:23 -05:00
|
|
|
refs = getattr(obj.aq_base, self.name, None)
|
2011-10-04 13:12:58 -05:00
|
|
|
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
|
2012-03-02 08:42:22 -06:00
|
|
|
if objects:
|
|
|
|
self.linkObject(obj, objects)
|
2010-10-14 07:43:56 -05:00
|
|
|
|
2012-06-02 07:36:49 -05:00
|
|
|
def mayAdd(self, obj):
|
|
|
|
'''May the user create a new referred object from p_obj via this Ref?'''
|
2012-03-27 08:49:41 -05:00
|
|
|
# We can't (yet) do that on back references.
|
2012-06-21 04:31:27 -05:00
|
|
|
if self.isBack: return No('is_back')
|
2012-03-27 08:49:41 -05:00
|
|
|
# Check if this Ref is addable
|
|
|
|
if callable(self.add):
|
|
|
|
add = self.callMethod(obj, self.add)
|
|
|
|
else:
|
|
|
|
add = self.add
|
2012-06-21 04:31:27 -05:00
|
|
|
if not add: return No('no_add')
|
2012-03-27 08:49:41 -05:00
|
|
|
# Have we reached the maximum number of referred elements?
|
|
|
|
if self.multiplicity[1] != None:
|
|
|
|
refCount = len(getattr(obj, self.name, ()))
|
2012-06-21 04:31:27 -05:00
|
|
|
if refCount >= self.multiplicity[1]: return No('max_reached')
|
2012-03-27 08:49:41 -05:00
|
|
|
# May the user edit this Ref field?
|
2012-06-21 04:31:27 -05:00
|
|
|
if not obj.allows(self.writePermission): return No('no_write_perm')
|
2012-06-02 07:36:49 -05:00
|
|
|
# Have the user the correct add permission?
|
2012-03-27 08:49:41 -05:00
|
|
|
tool = obj.getTool()
|
|
|
|
addPermission = '%s: Add %s' % (tool.getAppName(),
|
|
|
|
tool.getPortalType(self.klass))
|
2012-06-02 07:36:49 -05:00
|
|
|
folder = obj.getCreateFolder()
|
2012-06-21 04:31:27 -05:00
|
|
|
if not obj.getUser().has_permission(addPermission, folder):
|
|
|
|
return No('no_add_perm')
|
2012-03-27 08:49:41 -05:00
|
|
|
return True
|
|
|
|
|
2012-06-02 07:36:49 -05:00
|
|
|
def checkAdd(self, obj):
|
|
|
|
'''Compute m_mayAdd above, and raise an Unauthorized exception if
|
|
|
|
m_mayAdd returns False.'''
|
2012-06-21 04:31:27 -05:00
|
|
|
may = self.mayAdd(obj)
|
|
|
|
if not may:
|
2012-06-02 07:36:49 -05:00
|
|
|
from AccessControl import Unauthorized
|
2012-06-21 04:31:27 -05:00
|
|
|
raise Unauthorized("User can't write Ref field '%s' (%s)." % \
|
|
|
|
(self.name, may.msg))
|
2012-06-02 07:36:49 -05:00
|
|
|
|
2012-10-07 04:26:55 -05:00
|
|
|
def changeOrderEnabled(self, obj):
|
|
|
|
'''Is changeOrder enabled?'''
|
|
|
|
if isinstance(self.changeOrder, bool):
|
|
|
|
return self.changeOrder
|
|
|
|
else:
|
|
|
|
return self.callMethod(obj, self.changeOrder)
|
|
|
|
|
2012-08-21 12:57:23 -05:00
|
|
|
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)
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
class Computed(Type):
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
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,
|
2009-10-30 15:31:39 -05:00
|
|
|
specificWritePermission=False, width=None, height=None,
|
2011-05-05 09:44:06 -05:00
|
|
|
maxChars=None, colspan=1, method=None, plainText=True,
|
|
|
|
master=None, masterValue=None, focus=False, historized=False,
|
2012-12-14 02:23:33 -06:00
|
|
|
sync=True, mapping=None, label=None, sdefault='', scolspan=1,
|
2013-02-19 02:57:02 -06:00
|
|
|
swidth=None, sheight=None, context={}):
|
2010-08-05 11:23:17 -05:00
|
|
|
# The Python method used for computing the field value
|
|
|
|
self.method = method
|
|
|
|
# Does field computation produce plain text or XHTML?
|
|
|
|
self.plainText = plainText
|
2011-01-28 07:36:30 -06:00
|
|
|
if isinstance(method, basestring):
|
|
|
|
# When field computation is done with a macro, we know the result
|
|
|
|
# will be HTML.
|
|
|
|
self.plainText = False
|
2011-01-28 19:18:14 -06:00
|
|
|
# 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
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
Type.__init__(self, None, multiplicity, default, show, page, group,
|
2013-02-19 02:57:02 -06:00
|
|
|
layouts, move, indexed, searchable,specificReadPermission,
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
specificWritePermission, width, height, None, colspan,
|
|
|
|
master, masterValue, focus, historized, sync, mapping,
|
2013-02-19 02:57:02 -06:00
|
|
|
label, sdefault, scolspan, swidth, sheight)
|
2010-08-05 11:23:17 -05:00
|
|
|
self.validable = False
|
|
|
|
|
2011-01-28 07:36:30 -06:00
|
|
|
def callMacro(self, obj, macroPath):
|
|
|
|
'''Returns the macro corresponding to p_macroPath. The base folder
|
2011-11-25 11:01:20 -06:00
|
|
|
where we search is "ui".'''
|
2011-01-28 07:36:30 -06:00
|
|
|
# Get the special page in Appy that allows to call a macro
|
2011-11-25 11:01:20 -06:00
|
|
|
macroPage = obj.ui.callMacro
|
2011-01-28 07:36:30 -06:00
|
|
|
# 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
|
2011-11-25 11:01:20 -06:00
|
|
|
page = obj.ui
|
2011-01-28 07:36:30 -06:00
|
|
|
for name in names[:-1]:
|
|
|
|
page = getattr(page, name)
|
|
|
|
macroName = names[-1]
|
2011-01-28 19:18:14 -06:00
|
|
|
# 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)
|
2011-01-28 07:36:30 -06:00
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
def getValue(self, obj):
|
|
|
|
'''Computes the value instead of getting it in the database.'''
|
2010-11-10 08:15:00 -06:00
|
|
|
if not self.method: return
|
2011-01-28 07:36:30 -06:00
|
|
|
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
|
2012-09-04 11:00:22 -05:00
|
|
|
return self.callMethod(obj, self.method, raiseOnError=True)
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2013-01-08 09:58:29 -06:00
|
|
|
def getFormattedValue(self, obj, value, showChanges=False):
|
2010-11-10 08:15:00 -06:00
|
|
|
if not isinstance(value, basestring): return str(value)
|
|
|
|
return value
|
2009-06-29 07:06:01 -05:00
|
|
|
|
|
|
|
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.'''
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
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,
|
2009-10-30 15:31:39 -05:00
|
|
|
specificWritePermission=False, width=None, height=None,
|
2011-05-05 09:44:06 -05:00
|
|
|
maxChars=None, colspan=1, action=None, result='computation',
|
|
|
|
confirm=False, master=None, masterValue=None, focus=False,
|
2011-09-06 14:46:57 -05:00
|
|
|
historized=False, mapping=None, label=None):
|
2010-08-05 11:23:17 -05:00
|
|
|
# Can be a single method or a list/tuple of methods
|
|
|
|
self.action = action
|
2011-02-12 10:09:11 -06:00
|
|
|
# For the 'result' param:
|
2010-11-26 10:30:46 -06:00
|
|
|
# * 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.
|
2011-02-12 10:09:11 -06:00
|
|
|
# * '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.
|
2010-11-26 10:30:46 -06:00
|
|
|
# * 'redirect' means that the action will lead to the user being
|
|
|
|
# redirected to some other page.
|
2010-08-05 11:23:17 -05:00
|
|
|
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
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
Type.__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,
|
2013-02-19 02:57:02 -06:00
|
|
|
label, None, None, None, None)
|
2010-08-05 11:23:17 -05:00
|
|
|
self.validable = False
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'}
|
2009-06-29 07:06:01 -05:00
|
|
|
def __call__(self, obj):
|
|
|
|
'''Calls the action on p_obj.'''
|
2012-08-21 12:57:23 -05:00
|
|
|
if type(self.action) in sequenceTypes:
|
|
|
|
# There are multiple Python methods
|
|
|
|
res = [True, '']
|
|
|
|
for act in self.action:
|
|
|
|
actRes = act(obj)
|
2009-06-29 07:06:01 -05:00
|
|
|
if type(actRes) in sequenceTypes:
|
2012-08-21 12:57:23 -05:00
|
|
|
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]
|
2009-06-29 07:06:01 -05:00
|
|
|
else:
|
2012-08-21 12:57:23 -05:00
|
|
|
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
|
2009-06-29 07:06:01 -05:00
|
|
|
return res
|
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
def isShowable(self, obj, layoutType):
|
|
|
|
if layoutType == 'edit': return False
|
|
|
|
else: return Type.isShowable(self, obj, layoutType)
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
class Info(Type):
|
|
|
|
'''An info is a field whose purpose is to present information
|
|
|
|
(text, html...) to the user.'''
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
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,
|
2009-10-30 15:31:39 -05:00
|
|
|
specificWritePermission=False, width=None, height=None,
|
2011-05-05 09:44:06 -05:00
|
|
|
maxChars=None, colspan=1, master=None, masterValue=None,
|
2011-09-06 14:46:57 -05:00
|
|
|
focus=False, historized=False, mapping=None, label=None):
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
Type.__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,
|
2013-02-19 02:57:02 -06:00
|
|
|
label, None, None, None, None)
|
2010-08-05 11:23:17 -05:00
|
|
|
self.validable = False
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2010-02-12 03:59:42 -06:00
|
|
|
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.'''
|
2012-11-26 06:58:27 -06:00
|
|
|
# Layout for rendering a POD field for exporting query results.
|
|
|
|
rLayouts = {'view': Table('fl', width=None)}
|
2011-02-16 06:43:58 -06:00
|
|
|
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.'
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
def __init__(self, validator=None, default=None, show=('view', 'result'),
|
2010-08-05 11:23:17 -05:00
|
|
|
page='main', group=None, layouts=None, move=0, indexed=False,
|
2010-02-12 03:59:42 -06:00
|
|
|
searchable=False, specificReadPermission=False,
|
|
|
|
specificWritePermission=False, width=None, height=None,
|
2011-05-05 09:44:06 -05:00
|
|
|
maxChars=None, colspan=1, master=None, masterValue=None,
|
2011-09-06 14:46:57 -05:00
|
|
|
focus=False, historized=False, mapping=None, label=None,
|
|
|
|
template=None, context=None, action=None, askAction=False,
|
|
|
|
stylesMapping={}, freezeFormat='pdf'):
|
2010-08-05 11:23:17 -05:00
|
|
|
# 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
|
2011-01-28 07:36:30 -06:00
|
|
|
# A global styles mapping that would apply to the whole template
|
|
|
|
self.stylesMapping = stylesMapping
|
2011-02-16 06:43:58 -06:00
|
|
|
# Freeze format is by PDF by default
|
|
|
|
self.freezeFormat = freezeFormat
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
Type.__init__(self, None, (0,1), default, show, page, group, layouts,
|
|
|
|
move, indexed, searchable, specificReadPermission,
|
2011-05-05 09:44:06 -05:00
|
|
|
specificWritePermission, width, height, None, colspan,
|
2011-09-06 14:46:57 -05:00
|
|
|
master, masterValue, focus, historized, False, mapping,
|
2013-02-19 02:57:02 -06:00
|
|
|
label, None, None, None, None)
|
2010-08-05 11:23:17 -05:00
|
|
|
self.validable = False
|
2010-02-12 03:59:42 -06:00
|
|
|
|
2011-02-16 06:43:58 -06:00
|
|
|
def isFrozen(self, obj):
|
|
|
|
'''Is there a frozen document for p_self on p_obj?'''
|
2011-10-11 10:32:23 -05:00
|
|
|
value = getattr(obj.o.aq_base, self.name, None)
|
2011-02-16 06:43:58 -06:00
|
|
|
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
|
2011-10-11 10:32:23 -05:00
|
|
|
fileName = getattr(obj.o.aq_base, self.name).filename
|
2011-02-16 06:43:58 -06:00
|
|
|
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)
|
2011-10-11 10:32:23 -05:00
|
|
|
res = getattr(obj.aq_base, self.name, None)
|
2011-02-16 09:10:59 -06:00
|
|
|
if res and res.size: return FileWrapper(res) # Return the frozen file.
|
2011-02-16 06:43:58 -06:00
|
|
|
# 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' % (
|
|
|
|
getOsTempFolder(), obj.uid, time.time(), outputFormat)
|
|
|
|
# Define parameters to give to the appy.pod renderer
|
2012-02-27 07:06:39 -06:00
|
|
|
podContext = {'tool': tool, 'user': obj.user, 'self': obj, 'field':self,
|
2011-02-16 06:43:58 -06:00
|
|
|
'now': obj.o.getProductConfig().DateTime(),
|
2011-07-07 02:43:16 -05:00
|
|
|
'_': obj.translate, 'projectFolder': tool.getDiskFolder()}
|
2011-02-16 06:43:58 -06:00
|
|
|
# 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.
|
2011-11-25 11:01:20 -06:00
|
|
|
objs = tool.o.executeQuery(obj.o.portal_type, searchName=search,
|
2011-02-16 06:43:58 -06:00
|
|
|
sortBy=sortKey, sortOrder=sortOrder, filterKey=filterKey,
|
|
|
|
filterValue=filterValue, maxResults='NO_LIMIT')
|
|
|
|
podContext['objects'] = [o.appy() for o in objs['objects']]
|
|
|
|
if specificContext:
|
|
|
|
podContext.update(specificContext)
|
|
|
|
# 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,
|
2012-01-04 11:03:46 -06:00
|
|
|
'stylesMapping': stylesMapping,
|
|
|
|
'imageResolver': tool.o.getApp()}
|
2011-02-16 06:43:58 -06:00
|
|
|
if tool.unoEnabledPython:
|
|
|
|
rendererParams['pythonWithUnoPath'] = tool.unoEnabledPython
|
|
|
|
if tool.openOfficePort:
|
|
|
|
rendererParams['ooPort'] = tool.openOfficePort
|
|
|
|
# Launch the renderer
|
|
|
|
try:
|
|
|
|
renderer = Renderer(**rendererParams)
|
|
|
|
renderer.run()
|
|
|
|
except appy.pod.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, FileWrapper):
|
2011-12-05 08:11:29 -06:00
|
|
|
value = value._zopeFile
|
2011-02-16 06:43:58 -06:00
|
|
|
setattr(obj, self.name, value)
|
|
|
|
|
2011-10-19 02:37:44 -05:00
|
|
|
class List(Type):
|
|
|
|
'''A list.'''
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
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,
|
2011-10-19 02:37:44 -05:00
|
|
|
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)):
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
Type.__init__(self, validator, multiplicity, default, show, page, group,
|
|
|
|
layouts, move, indexed, False, specificReadPermission,
|
|
|
|
specificWritePermission, width, height, None, colspan,
|
|
|
|
master, masterValue, focus, historized, True, mapping,
|
2013-02-19 02:57:02 -06:00
|
|
|
label, None, None, None, None)
|
2011-10-19 02:37:44 -05:00
|
|
|
self.validable = True
|
|
|
|
# Tuples of (names, Type 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
|
|
|
|
|
2012-03-01 10:35:23 -06:00
|
|
|
def getRequestValue(self, request, requestName=None):
|
2011-10-19 02:37:44 -05:00
|
|
|
'''Concatenates the list from distinct form elements in the request.'''
|
2012-03-01 10:35:23 -06:00
|
|
|
name = requestName or self.name # A List may be into another List (?)
|
|
|
|
prefix = name + '*' + self.fields[0][0] + '*'
|
2011-10-19 02:37:44 -05:00
|
|
|
res = {}
|
|
|
|
for key in request.keys():
|
|
|
|
if not key.startswith(prefix): continue
|
|
|
|
# I have found a row. Gets its index
|
2011-10-26 03:21:09 -05:00
|
|
|
row = Object()
|
2012-03-01 10:35:23 -06:00
|
|
|
if '_' in key: key = key[:key.index('_')]
|
2011-10-19 02:37:44 -05:00
|
|
|
rowIndex = int(key.split('*')[-1])
|
|
|
|
if rowIndex == -1: continue # Ignore the template row.
|
2012-03-01 10:35:23 -06:00
|
|
|
for subName, subField in self.fields:
|
|
|
|
keyName = '%s*%s*%s' % (name, subName, rowIndex)
|
|
|
|
v = subField.getRequestValue(request, requestName=keyName)
|
|
|
|
setattr(row, subName, v)
|
2011-10-19 02:37:44 -05:00
|
|
|
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.
|
2012-03-01 10:35:23 -06:00
|
|
|
request.set(name, res)
|
2011-10-19 02:37:44 -05:00
|
|
|
return res
|
|
|
|
|
|
|
|
def getStorableValue(self, value):
|
|
|
|
'''Gets p_value in a form that can be stored in the database.'''
|
2011-10-27 10:55:17 -05:00
|
|
|
res = []
|
2011-10-19 02:37:44 -05:00
|
|
|
for v in value:
|
2011-10-27 10:55:17 -05:00
|
|
|
sv = Object()
|
2011-10-19 02:37:44 -05:00
|
|
|
for name, field in self.fields:
|
2011-10-27 10:55:17 -05:00
|
|
|
setattr(sv, name, field.getStorableValue(getattr(v, name)))
|
|
|
|
res.append(sv)
|
|
|
|
return res
|
2011-10-19 02:37:44 -05:00
|
|
|
|
2011-10-26 03:21:09 -05:00
|
|
|
def getInnerValue(self, outerValue, name, i):
|
2011-10-19 02:37:44 -05:00
|
|
|
'''Returns the value of inner field named p_name in row number p_i
|
2011-10-26 03:21:09 -05:00
|
|
|
within the whole list of values p_outerValue.'''
|
2011-10-19 02:37:44 -05:00
|
|
|
if i == -1: return ''
|
2011-10-26 03:21:09 -05:00
|
|
|
if not outerValue: return ''
|
|
|
|
if i >= len(outerValue): return ''
|
|
|
|
return getattr(outerValue[i], name, '')
|
2011-10-19 02:37:44 -05:00
|
|
|
|
2012-05-05 10:04:19 -05:00
|
|
|
def getCss(self, layoutType, res):
|
2012-03-01 10:35:23 -06:00
|
|
|
'''Gets the CSS required by sub-fields if any.'''
|
|
|
|
for name, field in self.fields:
|
2012-05-05 10:04:19 -05:00
|
|
|
field.getCss(layoutType, res)
|
2012-03-01 10:35:23 -06:00
|
|
|
|
2012-05-05 10:04:19 -05:00
|
|
|
def getJs(self, layoutType, res):
|
2012-03-01 10:35:23 -06:00
|
|
|
'''Gets the JS required by sub-fields if any.'''
|
|
|
|
for name, field in self.fields:
|
2012-05-05 10:04:19 -05:00
|
|
|
field.getJs(layoutType, res)
|
2012-03-01 10:35:23 -06:00
|
|
|
|
2011-07-26 15:15:04 -05:00
|
|
|
# Workflow-specific types and default workflows --------------------------------
|
|
|
|
appyToZopePermissions = {
|
|
|
|
'read': ('View', 'Access contents information'),
|
|
|
|
'write': 'Modify portal content',
|
|
|
|
'delete': 'Delete objects',
|
|
|
|
}
|
|
|
|
|
2010-09-02 09:16:08 -05:00
|
|
|
class Role:
|
|
|
|
'''Represents a role.'''
|
2011-12-05 08:11:29 -06:00
|
|
|
zopeRoles = ('Manager', 'Owner', 'Anonymous', 'Authenticated')
|
|
|
|
zopeLocalRoles = ('Owner',)
|
|
|
|
zopeUngrantableRoles = ('Anonymous', 'Authenticated')
|
2010-09-02 09:16:08 -05:00
|
|
|
def __init__(self, name, local=False, grantable=True):
|
|
|
|
self.name = name
|
|
|
|
self.local = local # True if it can be used as local role only.
|
2011-12-05 08:11:29 -06:00
|
|
|
# It is a standard Zope role or an application-specific one?
|
|
|
|
self.zope = name in self.zopeRoles
|
|
|
|
if self.zope and (name in self.zopeLocalRoles):
|
2010-09-02 09:16:08 -05:00
|
|
|
self.local = True
|
|
|
|
self.grantable = grantable
|
2011-12-05 08:11:29 -06:00
|
|
|
if self.zope and (name in self.zopeUngrantableRoles):
|
2010-09-02 09:16:08 -05:00
|
|
|
self.grantable = False
|
|
|
|
# An ungrantable role is one that is, like the Anonymous or
|
|
|
|
# Authenticated roles, automatically attributed to a user.
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
class State:
|
2011-10-06 09:27:58 -05:00
|
|
|
def __init__(self, permissions, initial=False, phase=None, show=True):
|
2010-09-02 09:16:08 -05:00
|
|
|
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
|
2009-06-29 07:06:01 -05:00
|
|
|
self.initial = initial
|
|
|
|
self.phase = phase
|
|
|
|
self.show = show
|
2010-09-02 09:16:08 -05:00
|
|
|
# Standardize the way roles are expressed within self.permissions
|
|
|
|
self.standardizeRoles()
|
|
|
|
|
2011-07-26 15:15:04 -05:00
|
|
|
def getName(self, wf):
|
|
|
|
'''Returns the name for this state in workflow p_wf.'''
|
|
|
|
for name in dir(wf):
|
|
|
|
value = getattr(wf, name)
|
|
|
|
if (value == self): return name
|
|
|
|
|
2010-09-02 09:16:08 -05:00
|
|
|
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()
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
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
|
|
|
|
|
2011-07-26 15:15:04 -05:00
|
|
|
def updatePermission(self, obj, zopePermission, roleNames):
|
|
|
|
'''Updates, on p_obj, list of p_roleNames which are granted a given
|
2011-09-08 09:33:16 -05:00
|
|
|
p_zopePermission. This method returns True if the list has been
|
|
|
|
effectively updated.'''
|
2011-07-26 15:15:04 -05:00
|
|
|
attr = Permission.getZopeAttrName(zopePermission)
|
|
|
|
if not hasattr(obj.aq_base, attr) or \
|
2011-11-25 11:01:20 -06:00
|
|
|
(getattr(obj.aq_base, attr) != roleNames):
|
2011-07-26 15:15:04 -05:00
|
|
|
setattr(obj, attr, roleNames)
|
2011-09-08 09:33:16 -05:00
|
|
|
return True
|
|
|
|
return False
|
2011-07-26 15:15:04 -05:00
|
|
|
|
|
|
|
def updatePermissions(self, wf, obj):
|
|
|
|
'''Zope requires permission-to-roles mappings to be stored as attributes
|
|
|
|
on the object itself. This method does this job, duplicating the info
|
2011-09-08 09:33:16 -05:00
|
|
|
from this state definition on p_obj. p_res is True if at least one
|
|
|
|
change has been effectively performed.'''
|
|
|
|
res = False
|
2011-07-26 15:15:04 -05:00
|
|
|
for permission, roles in self.getPermissions().iteritems():
|
|
|
|
roleNames = tuple([role.name for role in roles])
|
|
|
|
# Compute Zope permission(s) related to this permission.
|
|
|
|
if appyToZopePermissions.has_key(permission):
|
|
|
|
# It is a standard permission (r, w, d)
|
|
|
|
zopePerm = appyToZopePermissions[permission]
|
|
|
|
elif isinstance(permission, basestring):
|
|
|
|
# It is a user-defined permission
|
|
|
|
zopePerm = permission
|
|
|
|
else:
|
|
|
|
# It is a Permission instance
|
|
|
|
appName = obj.getProductConfig().PROJECTNAME
|
|
|
|
zopePerm = permission.getName(wf, appName)
|
|
|
|
# zopePerm contains a single permission or a tuple of permissions
|
|
|
|
if isinstance(zopePerm, basestring):
|
2011-09-08 09:33:16 -05:00
|
|
|
changed = self.updatePermission(obj, zopePerm, roleNames)
|
|
|
|
res = res or changed
|
2011-07-26 15:15:04 -05:00
|
|
|
else:
|
|
|
|
for zPerm in zopePerm:
|
2011-09-08 09:33:16 -05:00
|
|
|
changed = self.updatePermission(obj, zPerm, roleNames)
|
|
|
|
res = res or changed
|
|
|
|
return res
|
2011-07-26 15:15:04 -05:00
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
class Transition:
|
2009-11-06 04:33:56 -06:00
|
|
|
def __init__(self, states, condition=True, action=None, notify=None,
|
2011-01-28 19:18:14 -06:00
|
|
|
show=True, confirm=False):
|
2009-06-29 07:06:01 -05:00
|
|
|
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
|
2010-09-02 09:16:08 -05:00
|
|
|
if isinstance(condition, basestring):
|
|
|
|
# The condition specifies the name of a role.
|
|
|
|
self.condition = Role(condition)
|
2009-06-29 07:06:01 -05:00
|
|
|
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.
|
2009-11-06 04:33:56 -06:00
|
|
|
self.show = show # If False, the end user will not be able to trigger
|
|
|
|
# the transition. It will only be possible by code.
|
2011-01-28 19:18:14 -06:00
|
|
|
self.confirm = confirm # If True, a confirm popup will show up.
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2011-07-26 15:15:04 -05:00
|
|
|
def getName(self, wf):
|
|
|
|
'''Returns the name for this state in workflow p_wf.'''
|
|
|
|
for name in dir(wf):
|
|
|
|
value = getattr(wf, name)
|
|
|
|
if (value == self): return name
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
def getUsedRoles(self):
|
2010-09-02 09:16:08 -05:00
|
|
|
'''self.condition can specify a role.'''
|
2009-06-29 07:06:01 -05:00
|
|
|
res = []
|
2010-09-02 09:16:08 -05:00
|
|
|
if isinstance(self.condition, Role):
|
|
|
|
res.append(self.condition)
|
2009-06-29 07:06:01 -05:00
|
|
|
return res
|
|
|
|
|
|
|
|
def isSingle(self):
|
2010-09-02 09:16:08 -05:00
|
|
|
'''If this transition is only defined between 2 states, returns True.
|
2009-06-29 07:06:01 -05:00
|
|
|
Else, returns False.'''
|
|
|
|
return isinstance(self.states[0], State)
|
|
|
|
|
2011-02-01 04:09:54 -06:00
|
|
|
def isShowable(self, workflow, obj):
|
|
|
|
'''Is this transition showable?'''
|
|
|
|
if callable(self.show):
|
|
|
|
return self.show(workflow, obj.appy())
|
|
|
|
else:
|
|
|
|
return self.show
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
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
|
|
|
|
|
2012-07-23 08:24:44 -05:00
|
|
|
def isTriggerable(self, obj, wf, noSecurity=False):
|
2011-07-26 15:15:04 -05:00
|
|
|
'''Can this transition be triggered on p_obj?'''
|
2011-09-09 10:39:58 -05:00
|
|
|
wf = wf.__instance__ # We need the prototypical instance here.
|
2011-07-26 15:15:04 -05:00
|
|
|
# Checks that the current state of the object is a start state for this
|
|
|
|
# transition.
|
2011-11-25 11:01:20 -06:00
|
|
|
objState = obj.State(name=False)
|
2011-07-26 15:15:04 -05:00
|
|
|
if self.isSingle():
|
|
|
|
if objState != self.states[0]: return False
|
|
|
|
else:
|
|
|
|
startFound = False
|
|
|
|
for startState, stopState in self.states:
|
|
|
|
if startState == objState:
|
|
|
|
startFound = True
|
|
|
|
break
|
|
|
|
if not startFound: return False
|
|
|
|
# Check that the condition is met
|
2011-11-25 11:01:20 -06:00
|
|
|
user = obj.getUser()
|
2011-07-26 15:15:04 -05:00
|
|
|
if isinstance(self.condition, Role):
|
|
|
|
# Condition is a role. Transition may be triggered if the user has
|
|
|
|
# this role.
|
2012-07-23 08:24:44 -05:00
|
|
|
if noSecurity: return True
|
2011-07-26 15:15:04 -05:00
|
|
|
return user.has_role(self.condition.name, obj)
|
|
|
|
elif type(self.condition) == types.FunctionType:
|
|
|
|
return self.condition(wf, obj.appy())
|
|
|
|
elif type(self.condition) in (tuple, list):
|
|
|
|
# It is a list of roles and/or functions. Transition may be
|
|
|
|
# triggered if user has at least one of those roles and if all
|
|
|
|
# functions return True.
|
|
|
|
hasRole = None
|
|
|
|
for roleOrFunction in self.condition:
|
|
|
|
if isinstance(roleOrFunction, basestring):
|
|
|
|
if hasRole == None:
|
|
|
|
hasRole = False
|
2012-07-23 08:24:44 -05:00
|
|
|
if user.has_role(roleOrFunction, obj) or noSecurity:
|
2011-07-26 15:15:04 -05:00
|
|
|
hasRole = True
|
|
|
|
elif type(roleOrFunction) == types.FunctionType:
|
|
|
|
if not roleOrFunction(wf, obj.appy()):
|
|
|
|
return False
|
|
|
|
if hasRole != False:
|
|
|
|
return True
|
|
|
|
|
|
|
|
def executeAction(self, obj, wf):
|
|
|
|
'''Executes the action related to this transition.'''
|
|
|
|
msg = ''
|
2011-09-09 10:39:58 -05:00
|
|
|
obj = obj.appy()
|
|
|
|
wf = wf.__instance__ # We need the prototypical instance here.
|
2011-07-26 15:15:04 -05:00
|
|
|
if type(self.action) in (tuple, list):
|
|
|
|
# We need to execute a list of actions
|
|
|
|
for act in self.action:
|
2011-09-09 10:39:58 -05:00
|
|
|
msgPart = act(wf, obj)
|
2011-07-26 15:15:04 -05:00
|
|
|
if msgPart: msg += msgPart
|
|
|
|
else: # We execute a single action only.
|
2011-09-09 10:39:58 -05:00
|
|
|
msgPart = self.action(wf, obj)
|
2011-07-26 15:15:04 -05:00
|
|
|
if msgPart: msg += msgPart
|
|
|
|
return msg
|
|
|
|
|
|
|
|
def trigger(self, transitionName, obj, wf, comment, doAction=True,
|
|
|
|
doNotify=True, doHistory=True, doSay=True):
|
|
|
|
'''This method triggers this transition on p_obj. The transition is
|
|
|
|
supposed to be triggerable (call to self.isTriggerable must have been
|
|
|
|
performed before calling this method). If p_doAction is False, the
|
|
|
|
action that must normally be executed after the transition has been
|
|
|
|
triggered will not be executed. If p_doNotify is False, the
|
2012-05-03 03:51:54 -05:00
|
|
|
email notifications that must normally be launched after the
|
2011-07-26 15:15:04 -05:00
|
|
|
transition has been triggered will not be launched. If p_doHistory is
|
|
|
|
False, there will be no trace from this transition triggering in the
|
|
|
|
workflow history. If p_doSay is False, we consider the transition is
|
|
|
|
trigger programmatically, and no message is returned to the user.'''
|
|
|
|
# Create the workflow_history dict if it does not exist.
|
|
|
|
if not hasattr(obj.aq_base, 'workflow_history'):
|
|
|
|
from persistent.mapping import PersistentMapping
|
|
|
|
obj.workflow_history = PersistentMapping()
|
|
|
|
# Create the event list if it does not exist in the dict
|
|
|
|
if not obj.workflow_history: obj.workflow_history['appy'] = ()
|
|
|
|
# Get the key where object history is stored (this overstructure is
|
|
|
|
# only there for backward compatibility reasons)
|
|
|
|
key = obj.workflow_history.keys()[0]
|
|
|
|
# Identify the target state for this transition
|
|
|
|
if self.isSingle():
|
|
|
|
targetState = self.states[1]
|
|
|
|
targetStateName = targetState.getName(wf)
|
|
|
|
else:
|
2011-11-25 11:01:20 -06:00
|
|
|
startState = obj.State(name=False)
|
2011-07-26 15:15:04 -05:00
|
|
|
for sState, tState in self.states:
|
|
|
|
if startState == sState:
|
|
|
|
targetState = tState
|
|
|
|
targetStateName = targetState.getName(wf)
|
|
|
|
break
|
2011-09-08 09:33:16 -05:00
|
|
|
# Create the event and add it in the object history
|
2011-07-26 15:15:04 -05:00
|
|
|
action = transitionName
|
|
|
|
if transitionName == '_init_': action = None
|
|
|
|
if not doHistory: comment = '_invisible_'
|
2011-09-08 09:33:16 -05:00
|
|
|
obj.addHistoryEvent(action, review_state=targetStateName,
|
|
|
|
comments=comment)
|
2011-07-26 15:15:04 -05:00
|
|
|
# Update permissions-to-roles attributes
|
|
|
|
targetState.updatePermissions(wf, obj)
|
2011-11-28 15:50:01 -06:00
|
|
|
# Reindex the object if required. Not only security-related indexes
|
|
|
|
# (Allowed, State) need to be updated here.
|
|
|
|
if not obj.isTemporary(): obj.reindex()
|
2011-07-26 15:15:04 -05:00
|
|
|
# Execute the related action if needed
|
|
|
|
msg = ''
|
|
|
|
if doAction and self.action: msg = self.executeAction(obj, wf)
|
|
|
|
# Send notifications if needed
|
2012-05-03 03:51:54 -05:00
|
|
|
if doNotify and self.notify and obj.getTool(True).mailEnabled:
|
|
|
|
sendNotification(obj.appy(), self, transitionName, wf)
|
2011-07-26 15:15:04 -05:00
|
|
|
# Return a message to the user if needed
|
|
|
|
if not doSay or (transitionName == '_init_'): return
|
2012-06-03 11:34:56 -05:00
|
|
|
if not msg: msg = obj.translate('object_saved')
|
2011-07-26 15:15:04 -05:00
|
|
|
obj.say(msg)
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
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
|
2010-08-12 04:56:42 -05:00
|
|
|
attribute "t1" of class A, write: WritePermission("A.t1") or
|
2009-06-29 07:06:01 -05:00
|
|
|
WritePermission("x.y.A.t1") if class A is not in the same module as
|
2010-08-12 04:56:42 -05:00
|
|
|
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.'''
|
2011-07-26 15:15:04 -05:00
|
|
|
|
|
|
|
allowedChars = string.digits + string.letters + '_'
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
def __init__(self, fieldDescriptor):
|
|
|
|
self.fieldDescriptor = fieldDescriptor
|
|
|
|
|
2011-07-26 15:15:04 -05:00
|
|
|
def getName(self, wf, appName):
|
|
|
|
'''Returns the name of the Zope permission that corresponds to this
|
|
|
|
permission.'''
|
|
|
|
className, fieldName = self.fieldDescriptor.rsplit('.', 1)
|
|
|
|
if className.find('.') == -1:
|
|
|
|
# The related class resides in the same module as the workflow
|
|
|
|
fullClassName= '%s_%s' % (wf.__module__.replace('.', '_'),className)
|
|
|
|
else:
|
|
|
|
# className contains the full package name of the class
|
|
|
|
fullClassName = className.replace('.', '_')
|
|
|
|
# Read or Write ?
|
|
|
|
if self.__class__.__name__ == 'ReadPermission': access = 'Read'
|
|
|
|
else: access = 'Write'
|
|
|
|
return '%s: %s %s %s' % (appName, access, fullClassName, fieldName)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def getZopeAttrName(zopePermission):
|
|
|
|
'''Gets the name of the attribute where Zope stores, on every object,
|
|
|
|
the tuple of roles who are granted a given p_zopePermission.'''
|
|
|
|
res = ''
|
|
|
|
for c in zopePermission:
|
|
|
|
if c in Permission.allowedChars: res += c
|
|
|
|
else: res += '_'
|
|
|
|
return '_%s_Permission' % res
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
class ReadPermission(Permission): pass
|
|
|
|
class WritePermission(Permission): pass
|
|
|
|
|
2010-02-05 08:39:52 -06:00
|
|
|
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
|
|
|
|
|
2011-07-26 15:15:04 -05:00
|
|
|
class WorkflowAnonymous:
|
|
|
|
'''One-state workflow allowing anyone to consult and Manager to edit.'''
|
|
|
|
mgr = 'Manager'
|
2012-02-16 11:13:51 -06:00
|
|
|
o = 'Owner'
|
2012-04-19 02:20:15 -05:00
|
|
|
active = State({r:(mgr, 'Anonymous', 'Authenticated'), w:(mgr,o),d:(mgr,o)},
|
|
|
|
initial=True)
|
2011-07-26 15:15:04 -05:00
|
|
|
|
|
|
|
class WorkflowAuthenticated:
|
|
|
|
'''One-state workflow allowing authenticated users to consult and Manager
|
|
|
|
to edit.'''
|
|
|
|
mgr = 'Manager'
|
2012-02-16 11:13:51 -06:00
|
|
|
o = 'Owner'
|
|
|
|
active = State({r:(mgr, 'Authenticated'), w:(mgr,o), d:(mgr,o)},
|
|
|
|
initial=True)
|
2011-07-26 15:15:04 -05:00
|
|
|
|
2012-05-05 10:04:19 -05:00
|
|
|
class WorkflowOwner:
|
|
|
|
'''One-state workflow allowing only manager and owner to consult and
|
|
|
|
edit.'''
|
|
|
|
mgr = 'Manager'
|
|
|
|
o = 'Owner'
|
|
|
|
active = State({r:(mgr, o), w:(mgr, o), d:mgr}, initial=True)
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
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.'''
|
2010-01-06 11:36:16 -06:00
|
|
|
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):
|
2010-08-05 11:23:17 -05:00
|
|
|
# (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.
|
2010-01-06 11:36:16 -06:00
|
|
|
self.methodName = methodName
|
|
|
|
|
2010-08-05 11:23:17 -05:00
|
|
|
def getText(self, obj, value, appyType):
|
2010-01-06 11:36:16 -06:00
|
|
|
'''Gets the text that corresponds to p_value.'''
|
2010-08-05 11:23:17 -05:00
|
|
|
for v, text in appyType.getPossibleValues(obj, withTranslations=True):
|
|
|
|
if v == value:
|
|
|
|
return text
|
|
|
|
return value
|
2009-06-29 07:06:01 -05:00
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
2010-09-02 09:16:08 -05:00
|
|
|
class Model: pass
|
|
|
|
class Tool(Model):
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
'''If you want to extend or modify the Tool class, subclass me.'''
|
2010-09-02 09:16:08 -05:00
|
|
|
class User(Model):
|
|
|
|
'''If you want to extend or modify the User class, subclass me.'''
|
2009-06-29 07:06:01 -05:00
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
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']
|
2010-09-02 09:16:08 -05:00
|
|
|
# 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
|
2009-06-29 07:06:01 -05:00
|
|
|
# People having one of these roles will be able to create instances
|
|
|
|
# of classes defined in your application.
|
|
|
|
self.defaultCreators = ['Manager', 'Owner']
|
2011-01-14 02:06:25 -06:00
|
|
|
# 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'
|
2012-07-27 04:01:35 -05:00
|
|
|
# Activate or not the button on home page for asking a new password
|
|
|
|
self.activateForgotPassword = True
|
2012-12-15 16:36:56 -06:00
|
|
|
# Enable session timeout?
|
|
|
|
self.enableSessionTimeout = False
|
2012-07-26 10:22:22 -05:00
|
|
|
# When using Ogone, place an instance of appy.gen.ogone.OgoneConfig in
|
|
|
|
# the field below.
|
|
|
|
self.ogone = None
|
2009-06-29 07:06:01 -05:00
|
|
|
# ------------------------------------------------------------------------------
|